潮流特區

焦點文章

【開箱】真實測評:快充+多設備兼容!一條搞掂嘅 CyberCom 3合1 充電數據線

科技新知
Cheers!・2025-07-18

上次去旅行的時候,小編大頭蝦到漏帶咗 AirPods 條充電線,差啲成程冇歌聽! 好彩問朋友借到線救命,點知發現佢只係帶咗一條線,就叉到晒所有設備!咁方便,小編返到澳門即刻入手同款!今日就同大家開箱呢條 CyberCom 三合一充電數據線 設計實淨有質感 充電線包裝設計走簡約風,拆開上手,線身用尼龍編織,摸落比普通膠線硬淨好多,而且冇咁易打結纏埋一舊!接口位用 鋁合金外殼,手感好之餘,耐插耐用仲唔怕爆殼。三合一設計,轉接口好順手。 第一眼感覺:抵玩實用! 實測開始!睇下係咪真係咁神? 實測1:「一線多用」,兼容性大考驗! 買佢最大原因,就係想旅行/出街唔使再帶幾條線!即刻拎身邊啲裝置試真啲: ✔︎ iPhone(Lightning) ✔︎ Samsung Galaxy(Type-C) ✔︎ 舊款Android手機(Micro-USB) ✔︎藍牙耳機、行動硬碟... 新舊裝置全部用到! 以後出街/旅行,真係帶一條就夠,慳位又唔怕漏帶! 實測2:極速充能!60W 快充有幾快? 賣點話支援 QC 快充,仲有 60W 高功率輸出!小編實際使用過,無論係iPhone、android定平板,都可以穩定充電,食個晏、打鋪機嘅時間,部電話就已經叉得七七八八!對手機重度使用者嚟講,真係救星! 記住配合支援快充嘅插頭,咁就更加有感。 實測3:480Mbps 高速傳檔,快到飛起! 除咗叉電,佢仲支援 480Mbps 高速數據傳輸!即刻試下將手機啲相片、影片過落電腦 Backup! 比佢過一條2GB嘅旅行片...哇!大約40秒搞掂,比起普通 USB 線真係快成幾倍,以後過相過片唔使再慢慢等,效率大提升! CyberCom 三合一充電數據線解決曬小編平時帶線嘅煩惱 一線三用,兼容性強:新舊裝置、叉電傳檔都搞掂。 60W 快充夠快夠力:叉電速度救急必備。 傳檔超快 (480Mbps):Backup 大檔案唔使再等。 一條線就集齊晒叉電、傳檔、兼容性同耐用性。呢家仲做緊特價,$79呢個價位(原價$99),性價比高,值得入手!以後出街,真係可以同「帶幾條線」講 Bye Bye 啦! 想知係邊度買? 澳門市場強勢上市,現在購買享特價 $79! 官方網站:www.cyberportcom.com 客服郵箱:info@cyberportcom.com

最新文章

為何 VueJS 除錯如此麻煩?

科技新知
MacauYeah・2025-11-04

前一次,筆者分享了VS code debugging frontend的好功能,也確實了coding anywhere並不是一個普通的notepad + language server就可以解決的事。我們還要考慮如何debugging (除錯)的問題。 雖然筆者知道 vscode 可以解決問題,但為何 最原始的 nodejs debugger 不能解決問題。如果node debugger 不能解決問題,那麼 vscode 又做了什麼,它可以解決問題?經過一輪的實驗,筆者懷心疑,也許,強大的並不是 vscode 本身,而瀏覽器才真正的做到 debugger 的功能。而 vscode 只是以更方便的方式,重現那些結果。 為何 backend 的 debugger 不發揮作用? 筆者舉例,現時有一個 vue 3 專案,使用官方建議的方式生成 $ npm create vue@latest 這個專案,在開發模式下,會以 vite 架起一個端口為 5173 的伺服器,讓開發人員可以經過瀏覽器看到vue內容。筆者一直都認為,只要在 vite 的指令中插入 inspect 參數,一切就可以成功,就像 nodejs 一樣,只要在開始時加入參數就可以。結果當然是不行的。 經過對比 VueDevTools 的參考功能,筆者發現了一個出發點的問題。vite 其實是一個伺服器級的程式,也許它只是負責把所以 vue + js 動態轉成常見 js,就像 webpack 一樣。我們想要設的中斷點,都不在它的程式上,所以 debug 參數也沒有用。實質,我們要加的中斷點,其實要在客戶端上,也就是瀏覽器上。那因此,VueDevTools 也不包括那些功能。它只是好好地記錄了每個 vue component 或 js 是如何被改寫的過程(就像被 webpack改寫的過程)。 官方又是用什麼來除錯的? 既然我們知道了問題所在,就要看看傳統的 javascript 又是如何除錯的。實際上,因為瀏覽器的配合,設立中斷點的功能,原來早就實現了。 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/debugger 只要我們在任何 javascript 地方,插入 “debugger;” 這個神奇的字,瀏覽器就會在inspect模式下,自動產生中斷點。之後,你可以控制瀏覽器進行watch / step into / step over 功能。絕對比console.log更有意義。 在發現了這個方法之後,回去找vue3的官方文件,驚訝地發現,它就是提議用這種方式進行除錯。 https://vuejs.org/guide/extras/reactivity-in-depth.html#reactivity-debugging 未解之謎 雖然我們找到了設定中斷點的方式,但對於vscode是如何做到客戶端、伺服器端通用這件事,筆者還是沒有了解到。就以現在的知訊來看,很大機會就是vscode操控了瀏覽器的除錯模式,把所有資訊都回傳了vscode本身。這也是解譯了為何vscode在起動debugger時,必需要由vscode自己叫起瀏覽器。而codeserver這類雲IDE無法叫起本地瀏覽器,就造成它無法運用除錯功能的原因。 有與趣為codeserver一起搵解決方案的朋友,可以到筆者的 https://github.com/macauyeah/AProgrammerPrepares ,以文字教學的方提交你的解決方案。 祝願大家可以早日實現coding 自由。

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