潮流特區

焦點文章

CyberCom3合1充電數據線 — 一條線搞掂晒,充電超方便!
科技新知
Lifemagtechie・2025-06-12

而家大家手上嘅電子產品多到數唔清,手機、平板、藍牙耳機、遊戲機……每部機都有自己嘅充電線,仲有啲好舊款用USB,iPhone用Lightning,最新又係Type-C,搞到充電時成日手忙腳亂,尤其去旅行,帶幾多條線都唔夠! 呢條 CyberCom3合1充電數據線,就係為咗解決呢個煩惱而生。佢集合多種接口於一身,無論你係用緊iPhone定Android,甚至其他電子裝置,都可以用一條線搞掂晒,方便又慳位,真係你嘅生活充電好幫手! 一條線,多種接口,無懼設備多 CyberCom3合1充電數據線支援 Micro USB、USB A、Type C 同 Lightning 四大接口,無論你係iPhone、Samsung定係其他品牌,甚至係藍牙耳機、遊戲機,都可以用呢條線充電同傳輸數據。旅行唔使再帶一大堆線,行李又有位可以擺多啲戰利品啦! 快充快傳,效率up up! • 支援QC快充技術,最高60W輸出,手機、平板、智能手錶都可以超快充滿,唔使等成日。 • 傳輸速度高達480Mbps,無論係工作文件定係娛樂影片,一link就傳,節省你寶貴時間。
 耐用又防纏繞,攜帶超方便 • 採用彈性TPE物料,線身唔易打結,收納方便,唔怕亂晒。 • 線材耐用又有彈性,日常用或者出街旅行都好啱用。
 產品小百科 • 1米長度,無論係屋企、公司定旅行都啱用。 • 建議零售價 $99,依家優惠價$79就可以帶走。
 由此開始,充電更快更簡單! 設計貼心,性能強勁,係你工作同生活嘅好拍擋。無論係手機、平板、遊戲機定藍牙耳機,呢條線都幫你搞掂晒充電同數據傳輸,令你嘅數碼生活更輕鬆自在。了解CyberCom3合1充電數據線嘅詳情同優惠,幫你打造更智能、更方便嘅充電新體驗!購買網站: www.cyberportcom.com查詢電郵: info@cyberportcom.com

最新文章

你開始寫 Spring Boot 測試案例了嗎?
科技新知
MacauYeah・2025-11-29

雖然筆者過往做 spring boot framework 教學中,都有滲入一些測試用例。筆者也曾經困惑了很長一段時間,所以就獨立開一個主題,聊一下筆者在實務上對spring boot test 的理解。 測試案例究竟測試什麼? 測試用例 (test case) 是確保你的程式碼正確性與穩定性的重要步驟,但在 framework 下,並不是所有功能都很容易寫成測試。所以在討論 framework 測試之前,釐清測試的本質。 我們正常編寫的程式,基本流程可以概括為: function input - business logic - function output 這意味著我們輸入某些資料(input),然後經過業務邏輯(business logic)的處理,最後產生結果輸出(output)。 我們的測試目標,其實就是確保業務邏輯正確。而我們的手段就是經檢查概定的輸入資料,核對輸出結果。 那麼只要我們可以生成輸入資料,就一定可以檢查輸出結果了吧?其實不是的,因為實務上的輸入和輸出沒有這麼簡單。筆者常接觸到的輸入輸出如下 輸入 function 輸入參數 系統狀態資料,例如:資料庫狀態、外部API結果。 輸出 function 輸出參數 寫入系統(影響到)的資料,例如:資料庫狀態、使用外部API時的輸入參數。 總之就是考慮了狀態機 (state machine) 的問題,每個狀態+外部輸入都是一個測試用例,然後核對狀態機去了下一個什麼狀態。 言下之意,我們就是暴力地生成輸入參數和模擬狀態資料,道理上就是可以進行測試。 Spring boot web framework 中,我們又會測試什麼?

docker swarm 回到最基礎的群集組建
科技新知
MacauYeah・2025-11-21

雖然筆者都知道,全世界在講 k8s ,全世界都叫筆者放棄 docker swarm,但無獨有偶,docker swarm 還是有使用的價值。 你只有單個服務在運行,只想要做冗餘或分流。快速地用 docker swarm 做最小可行性産品,推出市場。 傳統的HA功能做到了,但你沒有中央匯整日誌的功能。而你也不想把事情攪得太複雜,使用docker swarm 可以讓你在任何一個管理節點上查看不同 container 的日誌。 你的客戶只提供VM,他可能有自己的k8s平台,但不讓你使用。自建一套docker swarm ,先入場,事後擴展再要求客戶提供k8s,對於客戶來講,先證明系統是有價值的,在金錢成本上或能力上,一定是件比較可以接受的事。 筆者之前介紹過一系列的 docker swarm 教學,但生成群集的部份一直沒有做介紹。因為實在太簡單,所以一直都沒有收納在教學內容當中。但現在考慮其完整性,以及為了讓大家感受一下它有多簡單,所以重新寫了組建群集的步驟。 組成群集 以前各家不同的軟件,想要起一個群集,要左攪右攪,又要重啟。而docker swarm真的很簡單,只要各機中有 docker ,再在各機中順序打指令就好。 node 1 使用docker swarm init docker swarm join-token manager # node 1 > docker swarm init > docker swarm join-token manager To add a manager to this swarm, run the following command: docker swarm join --token SWMTKN-1-xxxxxxxxxxxxxxxxxxxxxxx xxx.xxx.xxx.xxx:2377 其餘的管理員節點就根據上述的提示,使用 docker swarm join --token SWMTKN-1-xxxxxxxxxxxxxxxxxxxxxxx xxx.xxx.xxx.xxx:2377 就好。只要總數的管理員節點有奇數個就可以了(包括當初的node 1)。即是1、3、5等都可以。這是因為在容錯的情況下,必需由管理節點作出多數決,才能容易地知道判斷是哪些節點出現問題。 如果不為容錯,只想增加可工作的機器,那麼我們只需要增加工作節點。我們可以在任何管理員節點生成docker swarm join-token worker > docker swarm join-token worker To add a worker to this swarm, run the following command: docker swarm join --token SWMTKN-1-yyyyyyyyyyyyyyyyyyyyyyy yyy.yyy.yyy.yyy:2377 若想要檢查各個節點的工作狀態,在管理員節點上執行 docker node ls 看到了。 docker node ls ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION xxxxxxxxxxxxxxxxxxxxxxxxx * node1hostname Ready Active Leader 28.5.1 yyyyyyyyyyyyyyyyyyyyyyyyy * node2hostname Ready Active Reachable 28.5.1 全部教學請見 https://macauyeah.github.io/AProgrammerPrepares/VMDockerNotes/SwarmModeCommandCN.html

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