spring boot

標籤:spring boot

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。 @ResponseStatusHttpStatus.FORBIDDEN public class CustomAuthenticationException extends RuntimeException public CustomAuthenticationException public CustomAuthenticationExceptionString message supermessage; 以後,任何一個地方拋出 CustomAuthenticationException (假設上層沒有人攔截)都會把該 Controller 的結果改為 http 403。Spring boot 也很聰明的,把異常中的 message 隱藏 ,免得有網安的問題。 若我們定義 Exception 時,沒有@ResponseStatus,Controller 就會變成 http 500,例如我在 controller 中拋個常見的 IOException,這次的結果就會變成 http 500。 @GetMappingquot;apiioErrorquot; public String forceIOException throws IOException throw new IOExceptionquot;force io errorquot;; 如果某些時候,我們想使用 java Exception 中的 message 欄位作為報錯信息,讓 http 客戶端,可以通過固定的 message 檔位找到問題訊息,我們可以在application.properties中,加入server.error.includemessage=always。有些特殊情況,在開發模式時 mvn springbootrun ,已經可以見到有 Exception message,但在投産後java jar又看不到。主要因為開發模式中, pom 有 optional springbootdevtools,會自動加入了server.error.includemessage=always,但 mvn package 後就沒有,因為 runtime 沒有 springbootdevtools 的覆蓋。 額外處理 異常處理除了想控制 http status code 外,有時還需要做一些額外處理,例如發出通知郵件等。若想做額外處理,需要另做一個 @RestControllerAdvice 的類,在接到指定的 exception 時,可以轉換不同的 http code ,而且還可以執行額外 java code ,改變 http ResponseBody 。 @RestControllerAdvice public class GlobalExceptionHandler @ExceptionHandlervalue = RuntimeException.class @ResponseStatusHttpStatus.INTERNAL_SERVER_ERROR public Map handleRuntimeExceptionException ex return Map.ofquot;retquot;, false, quot;anyfieldsquot;, ex.getMessage; 但要注意,一旦使用@RestControllerAdvice 後,就要考慮有沒有改變了某些預設的行為。例如上述的@ExceptionHandlervalue = RuntimeException.class,代表所有RuntimException.class的子類,都會歸由該 function 所處理。當然,你也可以多加幾個 function 來處理不同的子類。 Reference springbootwebapivalidate

比 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.putquot;mail.smtp.authquot;, quot;truequot;; props.putquot;mail.smtp.ssl.enablequot;, quot;truequot;; props.putquot;mail.smtp.socketFactory.portquot;, quot;465quot;; 一個最簡單可以連去 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; @Valuequot;$spring.mail.usernamequot; private String fromAddress; private static final Logger LOG = LoggerFactory.getLoggerSpringBootEmailApplicationTests.class; @Test void contextLoads try SimpleMailMessage mailMessage = new SimpleMailMessage; mailMessage.setFromfromAddress; mailMessage.setToquot;XXXXXXXXquot;; mailMessage.setTextquot;this is backend email trigger for spring bootquot;; mailMessage.setSubjectquot;spring boot test mailquot;; javaMailSender.sendmailMessage; catch Exception e LOG.errorquot;Error while Sending Mailquot;; throw new RuntimeExceptione; github 原始碼 httpsgithub.commacauyeahspringbootdemotreemainspringboottutorialspringbootemail

Spring Web 異步 Api

潮流特區
MacauYeah ・2025-10-18

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

升級 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 .securespec gt; spec.sslContextSslContextBuilder.forClient .defaultConfigurationSslProvider.DefaultConfigurationType.TCP .handshakeTimeoutDuration.ofSeconds30 .closeNotifyFlushTimeoutDuration.ofSeconds10 .closeNotifyReadTimeoutDuration.ofSeconds10; 觀看各大網站,都未有更新,唯有自行研究官方說明。 筆者撰寫本文的時候,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 .securespec gt; spec.sslContexthttp11SslContextSpec .handshakeTimeoutDuration.ofSeconds30 .closeNotifyFlushTimeoutDuration.ofSeconds10 .closeNotifyReadTimeoutDuration.ofSeconds10; WebClient webClient = WebClient.builder.clientConnectornew ReactorClientHttpConnectorhttpClient .build; ... 雖然這個寫法來看netty 1.2.6,但似乎1.1.x 通用。大家有需要可以交互測試一下。 Reference netty 1.2.6 httpclienttimeout 的設定 netty 1.1.30 timeoutconfiguration 的設定 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 層面指定。 @Tableschema = quot;ORIGINAL_SCHEMAquot; 這看上去,是很有彈性的。但其實也是有些局限。如果你本來的JPA有寫特制的 JPQL 或 Raw Query,又或者你在Java層面加了 @Subselect,由於這些都是程式原作者所 hard code 的,JPA沒法幫你改寫。改來改去,可能還是前述寫Script的方法,一口氣把所有 Table 給鎖起來實際一些。 Reference 更多筆者的程式開發分享,見請 github