潮流特區

焦點文章

「心寒影滙2」大人細路齊齊玩盡Halloween!

創意生活
Cheers!・2025-10-08

2025嘅萬聖節,澳門新濠影滙為您準備好「驚嚇」同「歡樂」雙重體驗~膽大王必挑戰嘅「心寒片場」可以一站式體驗以七大電影為主題的解謎鬼屋,沉浸式感受被鬼嚇住動腦!一家大細都啱玩嘅「玩『懼』萬聖節」Toy Story 主題攤位遊戲、面部彩繪、免費爆谷同「Trick or Treat」等緊您! 9月19日至11月2日,無論您係膽大王定係家庭樂,總有一樣啱您玩! 門票現已發售,立即預訂:https://s.ctm.net/jpvOc 心寒片場 (Creepy Studio) |解謎鬼屋等您體驗 平時睇鬼片就多,親身被鬼追您又試過未?今個萬聖節,新濠影滙「心寒片場」解謎鬼屋強勢回歸!七大恐怖電影主題輪住嚇您 —《娃鬼回魂》、《鬼新娘迎親》、《邪靈娃娃》…… 一邊解謎一邊逃出,包您嚇到尖叫唔停口! 日期:2025年9月19日至11月2日(逢星期五至日)時段: 下午 15:00 - 19:00(最後入場 18:30) 晚上 19:00 - 23:00(最後入場 22:30) 地點:澳門新濠影滙一樓(巨星酒廊對面) 膽大王想獨闖「心寒片場」?標準門票MOP $144即刻出發,新濠會員更可享9折優惠!如果自問「膽細細又驚又要玩」,不如拉埋班FD一齊挑戰四人行套票,MOP 444(原價MOP $576)平均每人只需MOP $111,抵玩又刺激~ 專程從香港過來玩?咁就一定要訂「港澳船票套票」啦!MOP $404即包「心寒片場」門票1張同香港澳門來回船票1套(「香港-澳門」船票僅限於門票當天或提前一日使用;「澳門-香港」需於門票當日計算七日內來使用。),原價成MOP $584,而家慳咗成百幾蚊真係超值! 玩完驚到餓?我哋幫您諗埋~「心寒片場」星滙自助午餐套票 MOP $404(原價MOP $538),包「心寒片場」門票同自助午餐1位 (*僅限門票當日或翌日使用) ;或者選擇「嘉宴」餐飲禮券套票 MOP 404(原價MOP $544),包「心寒片場」門票同MOP $400餐飲禮券,等您可以盡情補充能量!(*僅限活動期間使用) 無論獨闖、組隊、定係遠道而來,總有一套啱您玩!立即預訂:https://s.ctm.net/jpvOc 玩「懼」萬聖節|一家大細放心玩~遊戲攤位+面部彩繪+免費爆谷! 萬聖節小朋友Cos得咁得意,點可以唔帶佢哋出嚟威?今個萬聖節就去「澳門新濠影滙一樓時代廣場」!場內設有多個《反斗奇兵》主題攤位遊戲,MOP$20換一個遊戲代幣,挑戰成功即有機會贏得Toy Story獨家禮品,大人細路都玩得開心! 新濠會員尊享優惠: MOP $200 換10個遊戲代幣,免費送「反斗奇兵三眼仔糖果桶」,小朋友可以拎住去Trick or Treat啦! 面部彩繪體驗:用2個遊戲代幣,即可為小朋友畫超應節嘅萬聖節彩繪,可愛定恐怖任您揀! 免費爆谷派發:喺時代廣場(DFS 附近)仲有免費派發爆谷,首200名客人更可獲《反斗奇兵》主題爆谷,送完即止! 遊戲攤位開放時間:2025年9月19日至11月2日(逢星期五至日) 時間:13:00 - 21:00 (面部彩繪體驗 & 爆谷派發時間:13:00 – 18:00 ) 地點:澳門新濠影滙1樓時代廣場 Trick or Treat?|糖果大放送! 小朋友最愛環節嚟啦!9月19日至11月2日期間(每日都有!),只要喺新濠影滙精選商戶門口大叫「Trick or Treat?」,即可免費拎糖!實現糖果大豐收~ 萬聖節點止有得玩?新濠影滙「搞怪美食」驚喜登場! 玩到肚餓?唔使驚!新濠影滙四間餐廳齊推萬聖節主題限定套餐,搞怪得黎又好味,絕對係視覺與味覺嘅雙重享受~ 載運美式餐室嘅木乃伊開心套餐 / 骷髏骨頭開心套餐 / 惡魔魅影開心套餐,隨餐仲送萬聖節公仔掛飾,即時補充您係鬼屋尖叫流失嘅能量! 甜品控必衝羅浮餅廊及輕食嘅造型蛋糕, 小編最期待嘅就係「骷髏朱古力紅桑子蛋糕」同「紅桑子十字架丹麥 」,造型驚悚但味道驚喜,保證您相機食先! 想同朋友Chill 住打卡? 巨星酒廊萬聖節Tea Set絕對唔輸蝕 — 「怪獸之眼」、「吸血南瓜」、「木乃伊手指」、「女巫紅寶石」… ……款款精緻得嚟帶點詭異,包您一邊驚一邊笑! 最後當然唔少得星滙餐廳自助餐,多款萬聖主題美食任您放題,仲有一系列可愛到「嚇死人」嘅甜品,等您視覺同味覺同時被攻擊! 驚聲睇好戲|萬聖節電影優惠 喺鬼屋尖叫完,點可以唔睇返場戲壓壓驚?活動期間憑當日「心寒片場」門票,或者著住萬聖節服飾,去「影滙戲院」買飛即享: 第二張戲飛只要 MOP 60元! (即場出示當日「心寒片場」門票或穿著萬聖節主題服飾即享優惠) 一邊食爆谷一邊睇戲,萬聖節之旅完美收官~ 活動日期:2025年9月19日至11月2日地點:澳門新濠影滙戲院票務處 今個萬聖節唔使諗去邊,新濠影滙包大人同細路盡興而歸!記得約實朋友、帶埋一家大細嚟玩啦!

最新文章

Spring boot web api 異常處理

科技新知
MacauYeah・2025-10-28

我們在編寫程式時,經常會遇到一些極端的情況,不會經過 function 的方式回傳結果。例如一個 function 原本是提供讀檔功能,但用戶傳入的並不是一個有效的檔案路徑,又或是誰路徑權限不足,無法讀取。這些不正常的結果,並不是原本 function 所協定的回傳值。那麼,我們會拋出異常 Exception ,中斷所有被呼叫中的 function ,讓上層用戶去考慮怎樣處理這個問題。 在 Web API 中,這些 Exception 就更常見。要求用戶傳入的參數,用戶就是有時候少了幾個。覆寫資料的時候,原本的資料已被刪除。但我們現在是經過 Web Api,不能像過去一直向上拋出異常就能通知用戶。我們需要的,是把異常轉成對應的 Http Status Code,讓用戶端可以快速識別異常的類型。 java 異常對應 Http Response Code 其實在 spring boot web 中,要做轉譯,是很簡單的。在定義 java Exception的時候,若有@ResponseStatus,spring boot web 就會自動回應對應的 http error code。 @ResponseStatus(HttpStatus.FORBIDDEN) public class CustomAuthenticationException extends RuntimeException { public CustomAuthenticationException() { } public CustomAuthenticationException(String message) { super(message); } } 以後,任何一個地方拋出 CustomAuthenticationException (假設上層沒有人攔截)都會把該 Controller 的結果改為 http 403。Spring boot 也很聰明的,把異常中的 message 隱藏 ,免得有網安的問題。 若我們定義 Exception 時,沒有@ResponseStatus,Controller 就會變成 http 500,例如我在 controller 中拋個常見的 IOException,這次的結果就會變成 http 500。 @GetMapping("/api/ioError") public String forceIOException() throws IOException { throw new IOException("force io error"); } 如果某些時候,我們想使用 java Exception 中的 message 欄位作為報錯信息,讓 http 客戶端,可以通過固定的 message 檔位找到問題訊息,我們可以在application.properties中,加入server.error.include-message=always。(有些特殊情況,在開發模式時 mvn spring-boot:run ,已經可以見到有 Exception message,但在投産後java -jar又看不到。主要因為開發模式中, pom 有 optional spring-boot-devtools,會自動加入了server.error.include-message=always,但 mvn package 後就沒有,因為 runtime 沒有 spring-boot-devtools 的覆蓋。) 額外處理 異常處理除了想控制 http status code 外,有時還需要做一些額外處理,例如發出通知郵件等。若想做額外處理,需要另做一個 @RestControllerAdvice 的類,在接到指定的 exception 時,可以轉換不同的 http code ,而且還可以執行額外 java code ,改變 http ResponseBody 。 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(value = RuntimeException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Map handleRuntimeException(Exception ex) { return Map.of("ret", false, "anyfields", ex.getMessage()); } } 但要注意,一旦使用@RestControllerAdvice 後,就要考慮有沒有改變了某些預設的行為。例如上述的@ExceptionHandler(value = RuntimeException.class),代表所有RuntimException.class的子類,都會歸由該 function 所處理。當然,你也可以多加幾個 function 來處理不同的子類。 Reference spring-boot-web-api-validate

Visual Studio Code 才是 coding anywhere的基礎?

科技新知
MacauYeah・2025-10-25

筆者過去就有發表過使用 VM / docker + code server 作為 coding anywhere的基礎, 現時也有一直使用。code server 有效,但對於Web App 開發,仍有所不足。 那個藏在瀏覽器的IDE - Code Server 使用 code server 的好處,就是筆者只需要一個有瀏覽器的客戶端,就可以連線到雲上的VM中使用 code server 。不論多重的功夫,交給外部的雲去做,自己的客戶端就可以盡可能輕便。不想自己攪一套code server開發環境?github codespaces in browser 也是一個很類似的替代器。它也是隨時經雲建立一台專用的 VM,之後就可以經瀏覽器進行開發。 一切看來都很好,所有東西都可以在 VM / docker 中進行。如果你的 VM / docker,可以有齊所有除錯工具,應該就真萬能了。現實就是不太美好,因為雲上的 VM ,docker 中的容器,主要都是沒圖形介面的。如果你想要利用的除錯工具,例如 chrome,你就未必可以順利在 headless VM / docker conatiner 中安裝了。很多除錯工具,要麼就需要圖形介面,要麼就要有條件連到本地硬碟,所以筆者就 code server 本身,真的沒有太多解法。 Web App 開發,回到原始的基本步 - Visual Studio Code 回到原始的基本步,本地Visual Studio Code + VM / docker ,就好好地可以利用本地的 chrome 等進行 NodeJs 的除錯。它就跟本地Visual Studio Code + 本地開發類似,本地能用的 chrome,可以經過 vscode 連到 VM / docker 內,只要Remote Development 插件就可以了。筆者測試過,真的很簡單,vscode連線後,會在你的VM / docker 內,安裝一個很細的 client。然後其他事就像本地開發一樣了。Remote Development 除了用自己的VM外,官方還稱它可以連上github codespaces。筆者就未有詳細測試,有興趣的朋友可以建立一個codespaces看看。 雖然 Visual Studio Code 並沒有保證完整地解決所有問題,但至少它提供了一個橋樑可以作為接口開發。coding anywhere 還是有條件實現,只是我們的客戶端並不如一開始的單純,只少要有一個完整的桌面電腦環境OS ,可以做到 port forward,做一些簡單的對接。只是單純的移動端 Web 界面,就未能夠做到那些複雜的跨機轉譯。

比 Java Mail 更簡單的 Spring boot email

科技新知
MacauYeah・2025-10-24

使用 Spring boot 對接 SMTP gateway 發 email ,相對是容易的。 基本上,它就是會使用自建的 org.springframework.mail.javamail.* , 對接 javax.mail.* / jakarta.mail.* 以前的所有設定值 ,都可以經 spring.mail.properties.* 傳入 例如 spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.ssl.enable=true spring.mail.properties.mail.smtp.socketFactory.port=465 就等於過去 java.util.Properties props = new java.util.Properties(); props.put("mail.smtp.auth", "true"); props.put("mail.smtp.ssl.enable", "true"); props.put("mail.smtp.socketFactory.port", "465"); 一個最簡單可以連去 google smtp 的簡易 code 如下 ### application.properties spring.mail.host=smtp.gmail.com spring.mail.port=587 spring.mail.username= spring.mail.password= spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true // SpringBootEmailApplicationTests.java @SpringBootTest class SpringBootEmailApplicationTests { @Autowired private JavaMailSender javaMailSender; @Value("${spring.mail.username}") private String fromAddress; private static final Logger LOG = LoggerFactory.getLogger(SpringBootEmailApplicationTests.class); @Test void contextLoads() { try { SimpleMailMessage mailMessage = new SimpleMailMessage(); mailMessage.setFrom(fromAddress); mailMessage.setTo("XXXXXXXX"); mailMessage.setText("this is backend email trigger for spring boot"); mailMessage.setSubject("spring boot test mail"); javaMailSender.send(mailMessage); } catch (Exception e) { LOG.error("Error while Sending Mail"); throw new RuntimeException(e); } } } github 原始碼 https://github.com/macauyeah/spring-boot-demo/tree/main/spring-boot-tutorial/spring-boot-email

Spring Web 異步 Api

科技新知
MacauYeah・2025-10-18

在設計網頁應用時,總會有某些功能,是特別消耗時間的,例如我們的應用要提供報表,或長時間搜索。如果,我們的 Web Api 的連結,要強制客戶端等待結果,那麼中途斷線需要重做的機會就變得很高,客戶端的體驗一定不太好。 面對這些情況,我們最好就把原本一個 API 功能分為三個 API 去做。 工作生成 API 查詢狀態 API 查詢結果 API 如果大家有信心,可以把2和3混合在一起,對於客戶端,也是一件好事。不過,2,3 因為回傳的結構可能不一樣,分開處理,程式碼會更易讀。 以下,筆者就以一個模擬報表生成的應用,去解釋如何設計可以即時回傳的 API。 source code: spring-boot-web-api-async ReportController.java 詳細解析 假設我們有一個 ReportController,它負責處理與報告生成相關的 HTTP 請求,它提供三個核心 API 端點。 啟動報告生成端點 @PostMapping("/reportJob/create") public ResponseEntity createJob() { String uuid = String.format("%d_%s", (new Date()).getTime(), UUID.randomUUID().toString()); CompletableFuture.runAsync(() -> { try { orderStatus.put(uuid, PROCESSING); Thread.sleep(10000); // 10-second simulated delay reportService.genAndSaveReport(uuid); orderStatus.put(uuid, COMPLETED); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); return ResponseEntity .accepted() .header(HttpHeaders.LOCATION, "/reportJob/status/" + uuid) .body(Map.of("uuid", uuid, "status api", "/api/reportJob/status/" + uuid, "download api", "/api/reportJob/download/" + uuid)); } 運作原理: 立即生成唯一的 uuid 來標識這次任務 在 CompletableFuture.runAsync 運行長時間的操作。 API 本身即時回傳了 HTTP 202 (Accepted) 狀態,告訴客戶端請求已被接受但尚未完成 在回傳的結果中,還有提示可以查詢狀態和查詢結果的API。 這種設計避免了 HTTP Gateway Timeout,因為回應是即時的 。 檢查進度端點 @GetMapping("/reportJob/status/{uuid}") public ResponseEntity getStatus(@PathVariable("uuid") String uuid) { String status = orderStatus.get(uuid); if (status == null) return ResponseEntity.notFound().build(); if (COMPLETED.equals(status)) { // return ResponseEntity.status(HttpStatus.SEE_OTHER) return ResponseEntity.ok() .header(HttpHeaders.LOCATION, "/api/reportJob/download/" + uuid) .body(Map.of("status", COMPLETED)); } return ResponseEntity.status(HttpStatus.ACCEPTED) .body(Map.of("status", PROCESSING)); } } 單純以 map orderStatus.get(uuid) 查看狀態結果。這個map 必需是多線程下使用還是安全的 (ConcurrentHashMap)。 下載結果端點 @GetMapping("reportJob/download/{uuid}") public ResponseEntity download(@PathVariable("uuid") String uuid) { String status = orderStatus.get(uuid); if (status == null || !COMPLETED.equals(status)) { return ResponseEntity.notFound().build(); } else { // 下載檔案 } } } 如果大家並不計較是否需要重做失敗的請求,這個例子已經可以簡單地達到即時異步回傳的效果。如果大家還需求考慮請求是否有效完成,就需要用到 message queue 或其他 job server ,這就不是同一個網頁應用的操作範圍。 Reference source code: spring-boot-web-api-async Building a Long-Running Async REST API in Spring Boot (with 202 + 303 Status Codes)

git submodule 的那些坑

科技新知
MacauYeah・2025-09-26

submodule 設定 有些時候,我們並不想追蹤submodule的預設分支。對於初次新增時,我們可以 git submodule add -b YOUR_BRANCH REPO_URL_OR_RELATIVE_REPO_PATH git submodule add -b feature/devcontainer https://github.com/macauyeah/spring-boot-multiple-datasource.git git submodule add -b feature/devcontainer ../spring-boot-multiple-datasource 若在初始化後期,想改branch,可以直接修改設定檔。(首次做,還是建議使用指令方式加入,因為第一次總要把submodule整個歷史記錄取下來。) # file .gitmodules [submodule "spring-boot-multiple-datasource"] path = spring-boot-multiple-datasource url = https://github.com/macauyeah/spring-boot-multiple-datasource.git branch = YOUR_BRANCH 關於上述 url 的部份,如果是公開的倉庫,當然可以以完整的方式存取。例如你可以直寫 url = https://github.com/macauyeah/spring-boot-multiple-datasource.git。 若為私有倉庫,道理上要本機有權限存取才行,對於持續整合/持續部署就有些麻煩。正常解決方向就是 CI Server 有齊所有倉庫的存取權限,具體要根據不同 CI Server 的設定,有時候還要跨 Docker 的方式去接入。那是有夠麻煩的一件事。但若果 main module 與 sub module 剛好為同一個倉,我們也可以使用相對路勁來解決。 # file .gitmodules [submodule "spring-boot-multiple-datasource"] path = spring-boot-multiple-datasource url = ../spring-boot-multiple-datasource.git branch = YOUR_BRANCH 但這是有代價的,我們在本地 checkout 時,也必需要模疑類似的文件夾架構,也就是 sub module 也要獨立 checkout 。

Docker Swarm - Private Registry 私有影像倉庫

科技新知
MacauYeah・2025-09-10

在構建投産環境時,如果 server 群沒有互聯網,又或對私隱很有要求,需要自建一個最簡單的 registry ,可以用這個。當然,那台機第一次必需經互聯網。架起後就可以斷網,並由其他 client 提送新的 registry image更新。 Registry Server 起動方式 最簡單的起動方式,但什麼都不設定。 docker run -d -p 5000:5000 --name registry registry:3 若想要加入 SSL,讓你的 client 不會認為它是不安全的 registry ,最簡易可以寫成 docker compose, 由 docker compose up -d 執行。 # docker-compose.yml registry: restart: always image: registry:3 ports: - 5000:5000 environment: REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt REGISTRY_HTTP_TLS_KEY: /certs/domain.key volumes: - /path/data:/var/lib/registry - /path/certs:/certs 上述的 environment 中,有條件的話,還請設定需要登入才能訪問限制。最簡單,可以使用 apache http header 驗證方式。 # docker-compose.yml registry: restart: always image: registry:3 ports: - 5000:5000 environment: REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt REGISTRY_HTTP_TLS_KEY: /certs/domain.key + REGISTRY_AUTH: htpasswd + REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd + REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm volumes: - /path/data:/var/lib/registry - /path/certs:/certs + - /path/auth:/auth REGISTRY_AUTH, REGISTRY_AUTH_HTPASSWD_PATH, REGISTRY_AUTH_HTPASSWD_REALM 的值照抄就好,然後/path/auth/htpasswd 就需要以 htpasswd 的格式提供內容 apache password_encryptions。即是以下那個樣子 USERNAME_1:BCRYPT_HASH_1 USERNAME_2:BCRYPT_HASH_2 USERNAME_3:BCRYPT_HASH_3 Client 連線方式 一切都設定好後,在 client 端,就可以登入並推送你的 image,(題外話,cli登入的都是以明文的方式存在電腦中,所以不要隨便在公開的地方存入自己的帳號) # login docker login YOUR_DOMAIN:5000 # try re-upload image docker image tag registry:3 YOUR_DOMAIN:5000/registry:3 docker image push YOUR_DOMAIN:5000/registry:3 如果 server 端沒有提供SSL,那麼 client 就只能設定 http 的不安全連線。 https://distribution.github.io/distribution/about/insecure/ 修改 client 端的 /etc/docker/daemon.json (Windows Docker Desktop請經 Gui修改),然後重啟 client 端的 docker { "insecure-registries" : ["YOUR_DOMAIN:5000"] } Registry Server 維護 - Garbage collection 垃圾回收 當我們設立了自己的 Registry 倉庫之後,少不免就是要維護硬碟的用量。很多過期的 Image ,沒有需要,那就手動刪除,然後進行 Garbage collection (垃圾回收)。另一種情況,就如前述教學中,大家使用統一版本號,例如 latest ,表面上看似只有一個 tag ,但其實底下可能已經藏有多個不同的版本,也需要經過Garbage collection來清理空間。 因為回收過程比較危險,所以官方並不建議自動做,以下就簡單講講為了做刪除和回收,設定檔要怎樣改。為方便改設定,我們更新 docker compose yaml 檔,把 server config 都帶到 container 外面。 registry: restart: always image: registry:3 ports: - 5000:5000 environment: REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt REGISTRY_HTTP_TLS_KEY: /certs/domain.key REGISTRY_AUTH: htpasswd REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm volumes: - /path/data:/var/lib/registry - /path/certs:/certs - /path/auth:/auth + - /path/config.yml:/etc/distribution/config.yml config.yml 就如下所示,為了提供 API 刪除 image 的可能,storage.delete.enbled 要為 true,又為著之後進行回收時,可以避免有人於回收中途上載,所以預先加入 storage.maintenance.readonly.enabled 的控制項。回收之前要把readonly改為true,回收後再調為false。 每次修改完,記得重啟一下 docker service 。 storage: filesystem: rootdirectory: /var/lib/registry delete: enabled: true maintenance: readonly: enabled: false Garbage collection 指令 # inside container # bin/registry garbage-collect [--dry-run] [--delete-untagged] [--quiet] /path/to/config.yml bin/registry garbage-collect --delete-untagged=true /etc/docker/registry/config.yml # outside container, at host level docker exec -it YOUR_CONATINER_NAME bin/registry garbage-collect --delete-untagged=true /etc/docker/registry/config.yml

升級 Spring Boot WebClient SSL (Reactor Netty 1.2.6):重新配置 SSL 設定

科技新知
MacauYeah・2025-08-27

因為SSL provider 更新了的關係,好多 HttpClient / WebClient 設定SSL的部份都要重寫以免出現 deprecated 問題 reactor.netty.http.client.HttpClient 在 1.0.x, 中可以這樣自行設定SSL逾時的部份,但當中的spec.sslContext().defaultConfiguration 在新版本,例如1.1.x後就會出現 deprecated。 // deprecated version HttpClient.create() .secure(spec -> spec.sslContext(SslContextBuilder.forClient()) .defaultConfiguration(SslProvider.DefaultConfigurationType.TCP) .handshakeTimeout(Duration.ofSeconds(30)) .closeNotifyFlushTimeout(Duration.ofSeconds(10)) .closeNotifyReadTimeout(Duration.ofSeconds(10))); 觀看各大網站,都未有更新,唯有自行研究官方說明。 筆者撰寫本文的時候,netty 發行版本為 1.2.6, 1.3.0 還里程碑(M6)的階段。所有參考皆來自1.2.6版本,實際上我們要使用新的後綴為ContextSpec類,看Class名應該有分http 1.1, 2, 3的版本,筆者就試用最基本的http 1.1。Http11SslContextSpec, (有條件的朋友可以試用Http2SslContextSpec, Http3SslContextSpec) import reactor.netty.http.Http11SslContextSpec; import reactor.netty.http.client.HttpClient; import java.time.Duration; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.http.client.reactive.ReactorClientHttpConnector; //... Http11SslContextSpec http11SslContextSpec = Http11SslContextSpec.forClient(); HttpClient httpClient = HttpClient.create() .secure(spec -> spec.sslContext(http11SslContextSpec) .handshakeTimeout(Duration.ofSeconds(30)) .closeNotifyFlushTimeout(Duration.ofSeconds(10)) .closeNotifyReadTimeout(Duration.ofSeconds(10))); WebClient webClient = WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)) .build(); //... 雖然這個寫法來看netty 1.2.6,但似乎1.1.x 通用。大家有需要可以交互測試一下。 Reference netty 1.2.6 http-client-timeout 的設定 netty 1.1.30 timeout-configuration 的設定 netty 1.2.6 java api doc netty release version 更多筆者的程式開發分享,見請 github

Spring Boot Web App 更新期間的維護模式:從唯讀到全鎖的解決方案

科技新知
MacauYeah・2025-08-25

在營運 Web App 的時候,雖然我們有 Docker / K8s 可以滾動更新,但難保用戶在更新的過程中,有一半訪問去到了舊版,另一半去了新版。如果可以,Web App 本身自帶維護模式,可以自我判斷什麼時候應該忽略新的訪問,當然最好。但要做到這一點,前期需要很多規劃。狠心一點,可以直接關掉對外的服務,讓用戶無法訪問。 但在另一些情況下,例如升級/搬遷的情況,下線時間比較長,完全關掉服務並不是一個很好的方向,我們至少還可以提供唯讀的選擇。而且這個可以從資料庫出發,讓 Web App 少處理一點邏輯。 如果 Web App 背後的資料庫是 MSSQL 或 MySQL,唯讀這件事應該是簡單的,只要你把 service account 的權限改變就好。但如果你用Oracle,就要想想辦法。 筆者想到的方法,暫時有兩個。第一個就需要大家寫寫 Script ,一口氣把所有 Table 給鎖起來。例如: 第二個,就是生成一個新的唯讀 User schema,給他所有Select的權限。然後更新 Web App 使用那個唯讀 User schema存取資料。 兩個方法有什麼差異呢? 前者就全部鎖起來,沒有任何一個資料庫用戶可以改寫資料。如果你的業務沒有差異性,全部一起封起來就完事。但如果你只想 Web App 轉成唯讀,但其他背景程式還可以執行更新。那你就只能用後者了。但後著也不是百分百的完全無痛,至少你 Web App 要支援登入與操作的 Schema分離。 例如用Spring boot JPA的話,可以在 application.properties 可以讓登入及操作的Schema不一樣。 spring.datasource.username=READ_ONLY_USER spring.jpa.properties.hibernate.default_schema=ORIGINAL_SCHEMA 又或者在 java 層面指定。 @Table(schema = "ORIGINAL_SCHEMA") 這看上去,是很有彈性的。但其實也是有些局限。如果你本來的JPA有寫特制的 JPQL 或 Raw Query,又或者你在Java層面加了 @Subselect,由於這些都是程式原作者所 hard code 的,JPA沒法幫你改寫。改來改去,可能還是前述寫Script的方法,一口氣把所有 Table 給鎖起來實際一些。 Reference 更多筆者的程式開發分享,見請 github

Galera 4 (Mariadb cluster) 的冷開機

科技新知
MacauYeah・2025-08-20

前次我們介紹了 Galera 4 在Ubuntu 24的架設方式,這次我們來補充一個最常見的問題Cold Start 冷開機 cold start 平常, Cluster 中只有其中一個 node 需要更新重啟,基本上所有節點回覆正常後,都可以互相通訊。而有些情況,例如斷電問題,需要所有節點全數關機,那麼 Galera cluster 就需要一定的方式重啟系統。那是一些狀態的保護機制,因為在全關機後再同步,系統不知道哪台機才有最新的狀態,它也不敢貿然同步(因為正常使用下, Galera cluster 只有兩台機也會開步)。所以需要人手介入,指定以某台機作為 cluster 的起始點。 舉個最簡單的例子,前述三台機 pocdbnode3 , pocdbnode2 , pocdbnode1 順序關閉,那麼 pocdbnode1 應該就會有最新的資訊。 在ubuntu中,可以查看 /var/lib/mysql/grastate.dat 中的 safe_to_bootstrap:是否為1。如果是1,代表當初它有最後的 transaction ,以它為起始點重新起 cluster。 $ cat /var/lib/mysql/grastate.dat # GALERA saved state version: 2.1 uuid: 0c38b6dd-7bdb-11f0-a4dd-1f4be36a6ea9 seqno: -1 safe_to_bootstrap: 1 我們使用galera_recovery, galera_new_cluster, 就可以把該機器重新救起mariadb process。 $ galera_recovery WSREP: Recovered position 0c38b6dd-7bdb-11f0-a4dd-1f4be36a6ea9:11 --wsrep_start_position=0c38b6dd-7bdb-11f0-a4dd-1f4be36a6ea9:11 $ galera_new_cluster 然後其餘兩個 node 可以直接重啟 mariadb 服務 # node 2 $ systemctl start mariadb # node 3 $ systemctl start mariadb Reference Getting Started with MariaDB Galera Cluster 官方文件 How to Set up MariaDB Galera Clusters on Ubuntu 22.04 How to Bootstrap MySQL or MariaDB Galera Cluster – Updated : 還有比較複雜的救機狀況,例如:safe_to_bootstrap全為0,即是可能是全部node都沒有好好地關掉,就掛了。大家有需要可以看看這個link的解決

Spring boot 10 - openapi 生成器 - spring boot java client

科技新知
MacauYeah・2025-08-19

之前我們在介紹Spring Boot Web 調試工具 ,就試安裝 openapi 相關的元件。其實 openapi 並不單是為了提供 swagger 測試介面,它主要是提供一個描述的方式,讓我們針對一個特定 openapi 文件,生成對應的 api server 或 api client 接口。也就是,如果 server 方有提供該文件,道理上可以經 openapi 的工具,生成一個可以直接訪問 server 的 client library。本節,可以沿用之前的 spring boot web api doc ,為它產生一個client library 作為實驗。 在生成 client library 之前,我們還需要一個工具 openapi-generator-cli 。最簡單的取得方式,就是經過 npm , 在你需要生成 client library 的專案中,安裝你需要的 openapi-generator-cli 版本。 npm install @openapitools/openapi-generator-cli 那怕你不是使用 nodejs 作為開發,也可以經過這個方法安裝。它只提供使用 cmd 指令的捷徑。 生成 Java Client Library 我們先把 backend server 起好 cd somewhere && mvn spring-boot:run,然後使用 openapi-generator-cli 去生成以 java spring boot 3 為底的 client library 。 npx openapi-generator-cli generate \ -i http://localhost:8080/v3/api-docs \ --api-package io.github.macauyeah.springboot.tutorial.openapiclient.api \ --model-package io.github.macauyeah.springboot.tutorial.openapiclient.model \ --invoker-package io.github.macauyeah.springboot.tutorial.openapiclient.invoker \ --group-id io.github.macauyeah.springboot.tutorial \ --artifact-id spring-boot-web-api-open-api-client \ --artifact-version 0.0.1-SNAPSHOT \ -g java \ -p useJakartaEe=true \ -p useSpringBoot3=true \ --library webclient \ -o spring-boot-web-api-open-api-client 生成的 source code 就像是 spring-boot-web-api-open-api-client ,具體的使用方式,可以看看測試用例 ApiControllerApiTest.java private final ApiControllerApi api = new ApiControllerApi(); @Test public void postDateQueryTest() { // default call ApiDateRequest apiDateRequest = new ApiDateRequest(); apiDateRequest.setInputDate(OffsetDateTime.now()); LOG.debug("default web client postDateQuery:{}", api.postDateQuery(apiDateRequest).block()); // replace webClient in ApiClient if you have special auth config on // webClient, you can also change basePath during new obj creation ObjectMapper mapper = new ObjectMapper(); mapper.setDateFormat(new SimpleDateFormat()); mapper.registerModule(new JavaTimeModule()); WebClient webClient = WebClient.builder() .codecs(configurer -> { configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper)); configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(mapper)); }) .build(); ApiControllerApi api2 = new ApiControllerApi( new ApiClient(webClient) .setBasePath("http://localhost:8080/")); LOG.debug("create api2 by local web client postDateQuery:{}", api2.postDateQuery(apiDateRequest).block()); // use webClient directly String response = webClient.post().uri("http://localhost:8080/api/record").bodyValue(apiDateRequest).retrieve() .bodyToMono(String.class).block(); LOG.debug("request by local web client postDateQuery:{}", response); } 上述例子中,如果大家沒有任何特殊要求,其實經過 api.postDateQuery(apiDateRequest).block() 就完成了。有需要改 api endpoint 的,只要生成新的 ApiClient 並設定 basePath new ApiClient().setBasePath("XXXXXX") 就好。真的要加入更多權限設定,就需要生成新的 ApiClient 並設定 webClient new ApiClient(webClient) 這個生成的 Java Client Library 道理上還是要經過 maven 等打包,變成 jar 檔,才能被其他 Java 專案所引用。筆者就建議大家直接把成生的視為獨立的 module (sub module) 存放,其他專案就以 maven dependency 的方式引用。想要混合現有專案,動態生成專案內某些 java package,暫時不太可行。因為它也有大量的 dependency ,交由 openapi-generator-cli 自己管理會比較好,它們升級時,你也可以完整升級。 openapi-generator-cli https://github.com/OpenAPITools/openapi-generator-cli spring-boot-web-api-open-api-client

生活AI 應用筆記

科技新知
MacauYeah・2025-07-22

上週六(7月19日),筆者參加了聖公會的一門講座「AI時代,父母的教養實戰新教程」,學習如何利用AI輔助小朋友的教育。雖然課題如此,但課堂還是以感受AI的使用方式為主,之後大家就好好利用AI這個知識庫去激發新思維。課堂上,文字、語音、動畫都有示範,但筆者感受到文字AI的部分最為深刻。 在過去一兩年,筆者曾略微使用AI,但即使在DeepSeek出現後,筆者仍覺得幫助有限。這主要是因為筆者身處科技行業,AI給出的答案不夠精準,難以協助開發,還可能導致一堆無法延續的結果,因此筆者甚少使用。然而,上課的主題並非針對本業使用AI,而是探討如何利用AI為生活注入更多新感受。 AI或許無法取代專業,但可以幫助你引入跨界元素,讓思考模式或你的作品更加多樣化。課堂中,學員將自己創作的家書放入AI,請它協助轉換成不同年齡層的人用詞、改編為劇本或變成辯論議題等。當然,並非每個方向都能產生合理的結果,例如家書轉為辯論,明顯會顯得不合適。但由於轉換的成本低,你可以透過少量提詞,得到多樣不同的呈現效果,激發新的思維。這就像在創作前,你可能會參考大量同類型的作品,去取得靈感。 筆者也簡單分享一些使用文字AI創作的方向,希望大家能有所收穫。 首先,準備好自己的原稿,這個原稿必須是由自己親自起草的,而非AI生成的主題方向。起草時,不必過於拘泥於前後文法,只需有一個大概方向即可。 將原稿交給AI,請它幫忙修改。AI會協助你修正一些口語或錯字問題。 嘗試請AI給你一些建議,或者請它幫你補充段落。筆者認為,請AI給建議會更好一些,因為有時補充段落可能會顯得過於機械。 除此之外,文字AI在日常生活中還做得不錯的實例 整理文章重點,重新以不同的方式演繹 整埋文章重點,筆者在大學的教育中,就經常看到以條列重點去取得原本書的教學方式。所以針對中小學教育,或新知識的學習,都可以試試用AI來整理大意,提升學習效率。 更強的是,如果你還是看不懂,可以叫AI以更多例子,深一點或淺一點去解釋一次。 禮物準備 有時物輕情義重,如果你身邊的親友很重儀式感,那麼使用AI的搜集及變化能力來幫你多樣化禮物的準備,相信可能為你加入更多新元素。 計劃旅行 計劃旅行,過去一般都參考他人的行程分享。過去就一篇一篇地逐篇閱讀,個人歸納。現在你可以先問AI,再去覆核。過去自行搜尋的方式,可能會被一些旅遊網站的文章所佔據,而現在改用AI,配合不同的提詞(prompt),你還可以多一些不同的參考角度。 註:本文僅使用AI修正用字,並未生成任何插畫或議題。