潮流特區

焦點文章

【開箱】真實測評:快充+多設備兼容!一條搞掂嘅 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

最新文章

codeserver 在團隊間開箱即用就是最大的好處
科技新知
MacauYeah・2025-11-20

之前我們就有探討過 vs code 與 codeserver 的差別,初步結論就是 vs code 的 debug 功能比較完善。如果大家懂得 devcontainer 的使用形式,使用 vs code 應該可以得到最大的效益。就在筆者想跳過 codeserver 的時候,又有新朋友對 codeserver 有興趣。最主要的原因還是它可以一體化預安裝所有事,若大家使用筆者的image,有 docker 、有瀏覽器就已經可以開箱即用。 所以這裏,筆者也重新翻新了筆者版本的使用說明。有興趣使用的朋友可以直接跟 github readme 試用。 https://github.com/wingzero0/codeserverUbuntu 本次翻新,主要加入了常見問題。這些問題部份與 docker 的基本限制有關、部份則是筆者的 env 所限。 常見問題 FAQ 運行 node 應用時很慢 在 windows / mac 下,它們的 docker 是經過 VM 建出來的。若使用 bind mount ,其實是經過 VM 層面抄資料夾。普通 java 開發沒有大問題,但如果遇上 node_module ,就會出現極大效能問題。 node_module 最好還是放在 container 內的 mounted volume 中。本 project 預設的 docker-compose.yaml 就已經有 /home/ubuntu/sourcecode mounted volume ,有需要可以放在其內直接使用。 linux 則沒有這個問題,因為 docker 只是 linux 的一個 process ,可以直接連到資料夾。 mounted volume 權限問題 如果大家自定義 mounted volume ,注意 docker 預設會是 root 權限,本系統使用 local user ubuntu,有需要改為它。 chown -R 'ubuntu:ubuntu' YOUR_TARGE_FOLDER 若然code-server異常,需要重啟。在 host 可以使用 docker command,在 container 中,可能殺掉所有 process # at host, outside of code-server docker compose -f docker-compose.local.yaml stop docker compose -f docker-compose.local.yaml start # at container, inside of code-server killall5 -9 上/下載 上載檔案:可以經過拖拉的方式,把桌面的檔案拖進 code-server 的 Explorer 區域。 下載檔案:可以點選 code-server Explorer區域內的檔案,按滑鼠右鍵,選 Download 。

為何 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的解決