今天繼續聊上手Quarkus框架這件事情,然後再簡單聊一下雲端,並做一個收尾
在依賴注入部分,沒有特別對@Produces做解釋,我覺得他不太好理解。所以這邊稍微紀錄解釋一下。
基本上你可以將@Produces
視為動態生成依賴的方法,它產生的 bean 的生命週期可以是任何 scope,包括 Dependent、ApplicationScoped、RequestScoped 等。它實現了 Factory Pattern,做到了 Factory Injection。什麼意思呢?簡單來說,Factory 會根據參數不同來建置 Instance,這與 ApplicationScope 單純在啟動時去 Instance 物件有很大的不同。這代表 @Produces
可以在 run time 動態地創建和注入物件,而這些物件的生命週期則由指定的 scope 和 CDI 容器來管理。
我覺得這段不好懂….如果對DI整個不熟的我個人覺得應該會看不太懂下面的範例,但我還是嘗試解釋看看。假設有一個情境,會根據使用者的訂閱級別來給與不同程度的Streaming
服務。我們針對此來動態判斷要回傳怎樣的Streaming
Instance。
先建置必要的物件如下
// StreamingService 介面
public interface StreamingService {
List<String> getAvailableMovies();
}
// Concrete class
public class BasicStreamingService implements StreamingService {
public List<String> getAvailableMovies() {
return Arrays.asList("Movie A", "Movie B");
}
}
public class PremiumStreamingService implements StreamingService {
public List<String> getAvailableMovies() {
return Arrays.asList("Movie A", "Movie B", "Exclusive Movie C", "Exclusive Movie D");
}
}
// User 類
public class User {
private String subscriptionLevel;
public User(String subscriptionLevel) {
this.subscriptionLevel = subscriptionLevel;
}
public String getSubscriptionLevel() {
return subscriptionLevel;
}
}
在一般我們沒有使用@Produces 方法,我們會建置一個StreamingServiceProducer
如下
public class StreamingServiceProducer {
@Inject
private User currentUser;
public StreamingService produceStreamingService() {
if ("PREMIUM".equals(currentUser.getSubscriptionLevel())) {
return new PremiumStreamingService();
} else {
return new BasicStreamingService();
}
}
}
而在實際的Context,我們就需要去涉略去使用produceStreamingService 來決定要Instance哪一個StreamingService
@Path("/movies")
@RequestScoped
public class MovieResource {
@Inject
private StreamingServiceProducer producer;
@Inject
private User currentUser;
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getMovies() {
StreamingService service = producer.produceStreamingService();
List<String> movies = service.getAvailableMovies();
return Response.ok(movies).build();
}
}
我們在produceStreamingService
加上 @Produces
與 @RequestScoped
註解,
@RequestScoped
public class StreamingServiceProducer {
@Inject
private User currentUser;
@Produces
@RequestScoped
public StreamingService produceStreamingService() {
if ("PREMIUM".equals(currentUser.getSubscriptionLevel())) {
return new PremiumStreamingService();
} else {
return new BasicStreamingService();
}
}
}
實際在使用上,MovieResource
不需要知道如何選擇正確的 StreamingService
,它只需要注入和使用。
@Path("/movies")
@RequestScoped
public class MovieResource {
@Inject
private StreamingService streamingService;
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getMovies() {
List<String> movies = streamingService.getAvailableMovies();
return Response.ok(movies).build();
}
}
簡單來說,@Produces
幫忙處裡掉Instance建置這段,讓MovieResource可以專注在streamingService的使用。
除了動態Instance判定,這樣的寫法帶來許多好處
@Produces
方法集中管理物件的建立邏輯,讓其他類別可以專注於使用這些物件,無需關心它們是如何被建立的。@Produces
方法中處理,簡化了使用方的程式碼。@Produces
方法,提供測試用的模擬物件。對於測試,以下是一個具體範例,可以看到我們輕鬆地模擬 StreamingService
,而不需要關心它實際上是如何被建立的。讓我們可以專注於測試 MovieResource
的行為,而不是 StreamingService
的建立邏輯。
// 正式環境程式碼
public class StreamingServiceProducer {
@Produces
@RequestScoped
public StreamingService produceStreamingService(User currentUser) {
if ("PREMIUM".equals(currentUser.getSubscriptionLevel())) {
return new PremiumStreamingService();
} else {
return new BasicStreamingService();
}
}
}
// 測試程式碼
@RunWith(MockitoJUnitRunner.class)
public class MovieResourceTest {
@Mock
private StreamingService mockStreamingService;
@InjectMocks
private MovieResource movieResource;
@Test
public void testGetMovies() {
// 設定模擬行為
when(mockStreamingService.getAvailableMovies()).thenReturn(Arrays.asList("測試電影"));
Response response = movieResource.getMovies();
List<String> movies = (List<String>) response.getEntity();
assertEquals(200, response.getStatus());
assertEquals(1, movies.size());
assertEquals("測試電影", movies.get(0));
}
}
在Quarkus,大部分是使用 CDI(Contexts and Dependency Injection)來處理中介軟體(Middleware)等擴展功能。雖然沒有一個像 .NET Core Startup.cs
這樣的明確入口點,但你可以通過多種方式來實現插入中介軟體功能,具體做法如下:
攔截器允許在方法執行之前或之後,插入額外的邏輯操作,比如記錄日誌、驗證使用者是否有權限執行某個操作,或者對結果進行後處理。
接著我們來透過 CDI具體來設計攔截器。使用 @Interceptor
註解來標記,並且必須實現 @AroundInvoke
方法。
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
import java.util.logging.Logger;
@Interceptor // 指定這個類為攔截器
@LoggingInterceptor // 使用自定義註解來啟用這個攔截器
public class LoggingInterceptor {
private static final Logger LOGGER = Logger.getLogger(LoggingInterceptor.class.getName());
/**
* 攔截方法,並在方法執行之前和之後執行一些邏輯
* @param context 方法的執行上下文
* @return 原本方法的返回結果
* @throws Exception 如果發生錯誤,拋出異常
*/
@AroundInvoke
public Object logMethodInvocation(InvocationContext context) throws Exception {
// 在方法執行之前記錄開始時間和方法名稱
LOGGER.info("方法執行開始: " + context.getMethod().getName());
// 繼續執行原始方法,並捕獲返回結果
Object result = context.proceed();
// 在方法執行之後記錄完成時間和方法名稱
LOGGER.info("方法執行完成: " + context.getMethod().getName());
// 返回原始方法的結果
return result;
}
}
@Interceptor
:標記這個類是一個攔截器。@AroundInvoke
:用來攔截方法執行的註解。InvocationContext
:提供對被攔截方法及與參數的訪問。proceed()
方法將會執行原始方法。為了在需要攔截的類別或方法上啟用攔截器,需要創建一個自訂的註解。這個註解的作用是讓 Quarkus 知道哪些地方需要應用攔截器邏輯。
import javax.interceptor.InterceptorBinding;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 定義一個攔截器綁定註解,讓其他類或方法可以使用
*/
@InterceptorBinding // 表示這個註解將會用於綁定攔截器
@Target({ElementType.METHOD, ElementType.TYPE}) // 註解可以應用於方法或類別
@Retention(RetentionPolicy.RUNTIME) // 註解在運行時可用
public @interface LoggingInterceptor {
}
@InterceptorBinding
:標記此註解是用來綁定攔截器的。@Target
:指定此註解可以用於方法或類別。@Retention
:定義這此註解的生存期,RUNTIME
意味著它在運行時依然有效。現在可以在需要的地方使用這個自訂的攔截器,來對方法的執行進行攔截。例如可以在 ExampleService
這個服務類別上使用。
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped // 表示這個類別是應用範圍的 CDI bean
public class ExampleService {
/**
* 這個方法將被攔截器攔截並記錄其執行情況
* @return 字串結果
*/
@LoggingInterceptor // 啟用我們定義的攔截器
public String performAction() {
return "操作已完成!";
}
}
過濾器是一個處理 HTTP 請求或回應的介面。可以在請求到達控制器之前,或回應發送給客戶端之前,進行額外的邏輯處理。這些處理通常包括驗證、記錄或修改請求/回應。
請求過濾器會攔截所有進入的 HTTP 請求,可以用它來記錄每個請求的相關資訊。範例如下
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
import java.util.logging.Logger;
@Provider // 告訴 Quarkus 這是一個 JAX-RS 的提供者
public class RequestLoggingFilter implements ContainerRequestFilter {
private static final Logger LOGGER = Logger.getLogger(RequestLoggingFilter.class.getName());
/**
* 在 HTTP 請求進入應用前攔截並記錄
* @param requestContext 請求上下文,包含請求相關資訊
* @throws IOException
*/
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
LOGGER.info("收到 HTTP 請求: " + requestContext.getUriInfo().getRequestUri());
}
}
ContainerRequestFilter
:用來定義處理請求邏輯。@Provider
:這個註解告訴 Quarkus 這是一個 JAX-RS 的組件,會自動註冊到框架中。回應過濾器可以在 HTTP 回應送出之前,對回應進行處理,比如改變狀態碼或修改回應內容。範例如下
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
import java.util.logging.Logger;
@Provider
public class ResponseLoggingFilter implements ContainerResponseFilter {
private static final Logger LOGGER = Logger.getLogger(ResponseLoggingFilter.class.getName());
/**
* 在 HTTP 回應發送給客戶端之前記錄回應資訊
* @param requestContext 請求上下文
* @param responseContext 回應上下文
* @throws IOException
*/
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
LOGGER.info("回應狀態碼: " + responseContext.getStatus());
}
}
過濾器會自動應用到所有 HTTP 請求和回應,無需額外配置。
至於如何定義執行順序這邊需使用@Priority
。數值越小,優先權越高,越先執行。舉個例子,
import javax.annotation.Priority;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
import java.util.logging.Logger;
@Provider
@Priority(100)
public class HighPriorityFilter implements ContainerRequestFilter {
private static final Logger LOGGER = Logger.getLogger(HighPriorityFilter.class.getName());
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
LOGGER.info("高優先權過濾器執行中");
}
}
@Provider
@Priority(200)
public class LowPriorityFilter implements ContainerRequestFilter {
private static final Logger LOGGER = Logger.getLogger(LowPriorityFilter.class.getName());
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
LOGGER.info("低優先權過濾器執行中");
}
}
HighPriorityFilter
的優先權值為 100,會優先執行。LowPriorityFilter
的優先權值為 200,會後續執行。另外如果要限制過濾器只應用於特定的路徑或資源。這可以通過使用 @PreMatching
註解或在過濾器類上使用 @Path
註解來實現。以下是範例,指定一個過濾器需在 JAX-RS 資源方法匹配之前執行。
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
import java.util.logging.Logger;
@Provider
@PreMatching
public class PreMatchingFilter implements ContainerRequestFilter {
private static final Logger LOGGER = Logger.getLogger(PreMatchingFilter.class.getName());
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
LOGGER.info("Pre-matching 過濾器執行中");
// 檢查請求的 User-Agent
String userAgent = requestContext.getHeaderString("User-Agent");
if (userAgent != null && userAgent.contains("OldBrowser")) {
// 如果是舊版瀏覽器,將請求重新導向到特定的資源
requestContext.setRequestUri(requestContext.getUriInfo().getBaseUriBuilder().path("legacy").build());
}
}
}
@PreMatching
註解確保這個過濾器會在 JAX-RS 進行資源方法匹配之前執行。檢查請求的 User-Agent 標頭。如果發現是舊版瀏覽器,它會將請求重新導向到一個特定的 "legacy" 路徑。
Quarkus 沒有一個單獨的文件作為所有中介軟體的配置入口,而是通過 CDI 注入和配置來靈活擴展應用的行為。
設置安全性(包括HTTPS、CORS、XSS防護和CSRF防護),
啟用HTTPS,需要設定SSL憑證和私鑰。具體步驟如下:
.pem
格式存在。application.properties
檔案中設定SSL相關配置quarkus.http.ssl.certificate.file=${SSL_CERT_FILE:server-cert.pem}
quarkus.http.ssl.certificate.key-file=${SSL_KEY_FILE:server-key.pem}
quarkus.http.ssl-port=${SSL_PORT:8443}
${}
語法表示這些值可以從環境變數中讀取
允許應用程式接收來自不同網域的請求,
quarkus.http.cors=true
quarkus.http.cors.origins=https://example.com
quarkus.http.cors.methods=GET,PUT,POST,DELETE
quarkus.http.cors.headers=accept,authorization,content-type
quarkus.http.cors.credentials=true
quarkus.http.cors.origins
定義允許的來源網域(白名單設置),例如 https://example.com
。quarkus.http.cors.methods
指定允許的HTTP方法,如 GET
, POST
等。quarkus.http.cors.headers
定義允許的請求標頭。quarkus.http.cors.credentials
若設置為 true
,則允許請求攜帶認證資訊(如Cookies)。透過惡意腳本在用戶端執行來竊取資料。Quarkus本身並不直接提供XSS的完整防護,但可透過以下方式進行防禦
**輸出編碼:**確保所有HTML內容在回應中進行編碼,避免惡意腳本執行。可以使用標準的Java工具,如 org.springframework.web.util.HtmlUtils
來進行轉義:
@GET
@Path("/safe")
public String getSafeContent() {
return HtmlUtils.htmlEscape("<script>alert('XSS')</script>");
}
HtmlUtils.htmlEscape
會將所有潛在的惡意腳本標籤轉義為無害的HTML字符,避免被瀏覽器執行。
CSRF是一種攻擊方式,攻擊者透過受害者的身份發起未經授權的操作。為防範CSRF攻擊,可以使用JWT(JSON Web Token)或OAuth2進行用戶驗證,以確保請求的合法性。
JWT 設定: Quarkus內建支援JWT,可以將JWT與API整合,確保每次請求都攜帶有效的認證令牌:
mp.jwt.verify.publickey.location=META-INF/resources/publicKey.pem
mp.jwt.verify.issuer=https://example.com
mp.jwt.verify.publickey.location
JWT公鑰的位置,用於驗證JWT。mp.jwt.verify.issuer
設定允許的發行者,確保只有來自可信來源的令牌才會被接受。Quarkus 預設支援多種日誌記錄框架,最常用的是 JBoss Log Manager,且與 SLF4J 以及 Logback 等常見的 Java 日誌框架也有不錯的整合性。
使用 SLF4J,加入相關的相依套件:
dependencies {
implementation 'org.slf4j:slf4j-api:2.0.0' // 或適合的版本
runtimeOnly 'org.slf4j:slf4j-jboss-logmanager:2.0.0' // 配合 SLF4J 的 JBoss Log Manager
}
使用 Logger
記錄日誌
不論是 JBoss Logging 還是 SLF4J,都可以用類似的方式來實作日誌紀錄功能。下面範例分別說明如何在Quarkus 應用中記錄日誌。
import org.jboss.logging.Logger;
public class MyService {
private static final Logger logger = Logger.getLogger(MyService.class);
public void execute() {
logger.info("系統正在正常運作");
try {
// 可能會拋出例外的邏輯
} catch (Exception e) {
logger.error("發生錯誤", e);
}
}
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyService {
private static final Logger logger = LoggerFactory.getLogger(MyService.class);
public void execute() {
logger.info("系統正在正常運作");
try {
// 可能會拋出例外的邏輯
} catch (Exception e) {
logger.error("發生錯誤", e);
}
}
}
透過 application.properties
檔案設定日誌的輸出層級,這樣可以控制哪些日誌訊息會被記錄,哪些會被忽略。以下是幾個常用的設定範例:
# 設定全域日誌層級為 INFO,這樣 INFO 級別以上的訊息才會被記錄
quarkus.log.level=INFO
# 設定某個特定套件的日誌層級為 DEBUG(例如針對 com.example)
quarkus.log.category."com.example".level=DEBUG
# 設定日誌的輸出格式(這裡是設定為時間、層級、類別、訊息)
quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n
Quarkus 提供額外的日誌功能,如 JSON 格式的日誌輸出或將日誌寫入檔案。這些功能可以方便日後與其他監控系統整合,例如 ELK(Elasticsearch, Logstash, Kibana)或 Prometheus。
quarkus.log.console.json=true
quarkus.log.file.enable=true
quarkus.log.file.path=logs/application.log
quarkus.log.file.level=INFO
透過修改 application.properties
,來啟用 JSON 日誌格式並自訂格式,例如符合 GCP 的要求。
# 啟用 JSON 格式的日誌輸出
quarkus.log.console.json=true
# 設定日誌的日期格式,GCP 通常要求 ISO 8601 標準的時間格式
quarkus.log.console.json.date-format=yyyy-MM-dd'T'HH:mm:ss.SSSZ
# 設定 JSON 日誌的欄位對應,這是為了符合 GCP 的結構化日誌
quarkus.log.console.json.fields.timestamp=true
quarkus.log.console.json.fields.level=true
quarkus.log.console.json.fields.thread=true
quarkus.log.console.json.fields.logger=true
quarkus.log.console.json.fields.message=true
# 可以加入更多欄位,根據需求擴充,例如加入 MDC 或 NDC 資訊
透過上述的設定,Quarkus 將會輸出類似以下的 JSON 結構:
{
"timestamp": "2024-10-01T14:23:34.123+0800",
"level": "INFO",
"thread": "main",
"logger": "com.example.MyService",
"message": "系統正在正常運作"
}
錯誤處理與例外管理的實作主要透過標準 Java 的例外機制與 Eclipse MicroProfile 錯誤容忍(Fault Tolerance)擴充功能進行管理,這些功能由 SmallRye 框架實現。
可以使用 JAX-RS 例外映射器(Exception Mappers)來處理 REST API 中的例外情況,並轉換成適當的 HTTP 回應。以下是捕捉 RuntimeException
並回傳自訂錯誤訊息的範例:
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.MediaType;
@Provider
public class MyExceptionMapper implements ExceptionMapper<RuntimeException> {
@Override
public Response toResponse(RuntimeException exception) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("伺服器錯誤,請稍後再試:" + exception.getMessage())
.type(MediaType.TEXT_PLAIN)
.build();
}
}
這個Expection Mapper會捕捉所有 RuntimeException
並回傳 500 錯誤代碼,且附上錯誤訊息。
以下是針對不同錯誤類型,對應不同ExceptionMapper處理範例
// 針對 IllegalArgumentException 的 Mapper
@Provider
public class IllegalArgumentExceptionMapper implements ExceptionMapper<IllegalArgumentException> {
@Override
public Response toResponse(IllegalArgumentException exception) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("無效的參數: " + exception.getMessage())
.type(MediaType.TEXT_PLAIN)
.build();
}
}
// 針對 NotFoundException 的 Mapper
@Provider
public class NotFoundExceptionMapper implements ExceptionMapper<NotFoundException> {
@Override
public Response toResponse(NotFoundException exception) {
return Response.status(Response.Status.NOT_FOUND)
.entity("找不到請求的資源: " + exception.getMessage())
.type(MediaType.TEXT_PLAIN)
.build();
}
}
// 針對 SQLException 的 Mapper
@Provider
public class SQLExceptionMapper implements ExceptionMapper<SQLException> {
@Override
public Response toResponse(SQLException exception) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("資料庫操作錯誤: " + exception.getMessage())
.type(MediaType.TEXT_PLAIN)
.build();
}
}
// 通用的 RuntimeException Mapper(作為後備)
@Provider
public class GenericExceptionMapper implements ExceptionMapper<RuntimeException> {
@Override
public Response toResponse(RuntimeException exception) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("發生未預期的錯誤: " + exception.getMessage())
.type(MediaType.TEXT_PLAIN)
.build();
}
}
// 1. 定義自定義業務異常
public class CustomBusinessException extends Exception {
private final String errorCode;
public CustomBusinessException(String message, String errorCode) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
// 2. 實現 CustomBusinessExceptionMapper
@Provider
public class CustomBusinessExceptionMapper implements ExceptionMapper<CustomBusinessException> {
@Override
public Response toResponse(CustomBusinessException exception) {
ErrorResponse errorResponse = new ErrorResponse(
exception.getErrorCode(),
exception.getMessage()
);
return Response.status(Response.Status.BAD_REQUEST)
.entity(errorResponse)
.type(MediaType.APPLICATION_JSON)
.build();
}
}
// 3. 定義錯誤回應對象
public class ErrorResponse {
private String errorCode;
private String errorMessage;
// 構造函數、getter 和 setter
}
// 4. 在業務邏輯中使用自定義異常
@Path("/orders")
public class OrderResource {
@Inject
OrderService orderService;
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createOrder(OrderRequest orderRequest) {
try {
Order createdOrder = orderService.createOrder(orderRequest);
return Response.status(Response.Status.CREATED)
.entity(createdOrder)
.build();
} catch (CustomBusinessException e) {
// 不需要在這裡處理異常,ExceptionMapper 會自動處理
throw e;
}
}
}
// 5. 服務層實現
@ApplicationScoped
public class OrderService {
public Order createOrder(OrderRequest orderRequest) throws CustomBusinessException {
if (orderRequest.getQuantity() <= 0) {
throw new CustomBusinessException("訂單數量必須大於零", "INVALID_QUANTITY");
}
// 其他業務邏輯...
if (/* 某些業務規則不滿足 */) {
throw new CustomBusinessException("訂單創建失敗:庫存不足", "INSUFFICIENT_STOCK");
}
// 創建訂單的邏輯...
return new Order(/* 訂單詳情 */);
}
}
Quarkus 支援 MicroProfile Fault Tolerance(錯誤容忍),這包含如重試、斷路器、逾時等機制。以下範例展示如何使用重試(@Retry)與斷路器(@CircuitBreaker):
import org.eclipse.microprofile.faulttolerance.CircuitBreaker;
import org.eclipse.microprofile.faulttolerance.Retry;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@Path("/fault-tolerance")
public class FaultToleranceResource {
@GET
@Path("/retry")
@Produces(MediaType.TEXT_PLAIN)
@Retry(maxRetries = 3, delay = 1000)
public String retryEndpoint() {
System.out.println("執行操作...");
throw new RuntimeException("操作失敗,重試中...");
}
@GET
@Path("/circuit-breaker")
@Produces(MediaType.TEXT_PLAIN)
@CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.75, delay = 5000)
public String circuitBreakerEndpoint() {
System.out.println("執行操作...");
throw new RuntimeException("操作失敗,斷路器啟動...");
}
}
retryEndpoint
方法會在失敗後重試 3 次,而 circuitBreakerEndpoint
方法會在達到 75% 的失敗率後啟動斷路器,並在 5 秒後恢復嘗試。
Quarkus 提供了內建的 quarkus-oidc
擴充套件,能夠讓應用程式使用 OIDC 協議進行使用者或服務的身份驗證。並且可以很方便地整合常見的身份提供者(如 Keycloak、Google 身份驗證等)。
dependencies {
implementation enforcedPlatform("io.quarkus:quarkus-bom:3.0.0.Final")
implementation 'io.quarkus:quarkus-oidc'
}
application.properties
中配置 OIDCquarkus.oidc.auth-server-url=https://你的身份提供者.com/realms/你的領域
quarkus.oidc.client-id=你的-client-id
quarkus.oidc.credentials.secret=你的-client-secret
quarkus.oidc.application-type=web-app
quarkus.oidc.roles.source=realm # 可以是 "realm" 或 "jwt" 根據角色來源
@RolesAllowed
註解import javax.annotation.security.RolesAllowed;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
@Path("/api")
public class MyResource {
@GET
@Path("/public")
public Response publicEndpoint() {
return Response.ok("這是公開的 API").build();
}
@GET
@Path("/protected")
@RolesAllowed("user") // 只有 "user" 角色的使用者可以存取
public Response protectedEndpoint() {
return Response.ok("這是受保護的 API,僅限具備 'user' 角色的使用者").build();
}
@GET
@Path("/admin")
@RolesAllowed("admin") // 只有 "admin" 角色的使用者可以存取
public Response adminEndpoint() {
return Response.ok("這是管理者 API,僅限具備 'admin' 角色的使用者").build();
}
}
application.properties
配置範例
# 指定 OIDC 伺服器的 URL,這裡通常是使用的身份提供者的地址,例如 Keycloak
quarkus.oidc.auth-server-url=https://你的身份提供者.com/realms/你的領域
# 用戶端 ID,身份提供者中為應用服務創建的 ID
quarkus.oidc.client-id=你的-client-id
# 用戶端密鑰,身份提供者中為應用程式提供密鑰
quarkus.oidc.credentials.secret=你的-client-secret
# 設定應用為 Web 應用,這樣 Quarkus 會自動處理 Web 端的登入、登出等邏輯
quarkus.oidc.application-type=web-app
# 登入和登出回調 URL
quarkus.oidc.authentication.redirect-path=/callback
quarkus.oidc.logout.path=/logout
# 配置是否允許被動驗證(如用於無頭服務器等)
quarkus.oidc.authentication.passive-enabled=true
# 設定自動跳轉到登入頁面,當未登入的使用者存取受保護資源時自動引導至身份提供者的登入頁面
quarkus.oidc.authentication.force-redirect-uris=/protected/*
# 配置默認角色來源
quarkus.oidc.roles.source=realm
設置會自動將未登入的使用者跳轉到身份提供者的登入頁面,並在成功登入後返回 Quarkus 應用,完成 SSO 流程。
Quarkus 沒有內建的 SAML 支援,可以透過 Keycloak 或 Pac4j 來實現 SAML 身份認證。這裡就很繁瑣…算是進階設計使用,我就直接PASS。
在Quarkus中,單元測試的實作主要使用JUnit 5,並結合Quarkus特有的功能來加速測試過程。
在Quarkus中,若需要進行單元測試,通常會使用@QuarkusTest
來標示該測試類別。這個註解會啟動一個最小的Quarkus應用程式上下文,方便進行測試。
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
@QuarkusTest
public class MyServiceTest {
@Test
public void testGetEndpoint() {
}
}
@InjectMock
來模擬服務,確保測試是針對單一功能進行,不會被其他外部依賴干擾。import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import javax.inject.Inject;
@QuarkusTest
public class GreetingServiceTest {
@InjectMock
GreetingService greetingService;
@Inject
MyService myService;
@Test
public void testGreetingService() {
Mockito.when(greetingService.greet()).thenReturn("Mocked Hello");
String response = myService.getGreeting();
assertEquals("Mocked Hello", response);
}
}
使用RestAssured測試RESTful API
在Quarkus中,可以用RestAssured
來簡化HTTP端點的測試。以下範例測試了GET端點的回應狀態碼和回傳內容是否符合預期:
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
@QuarkusTest
public class ExampleResourceTest {
@Test
public void testHelloEndpoint() {
given()
.when().get("/hello")
.then()
.statusCode(200)
.body(is("Hello RESTEasy"));
}
}
上述為測試應用程式中的/hello
這個REST端點是否:
可以看到他符合測試的Give-When-Then小黃瓜Gherkin 測試架構
使用Mockito模擬依賴
在測試過程中,有時需要模擬物件的行為,特別是當該物件涉及外部服務或複雜邏輯時。Mockito
可用來達成這項工作
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import javax.inject.Inject;
@QuarkusTest
public class MockServiceTest {
@InjectMock
ExternalService externalService;
@Inject
BusinessService businessService;
@Test
public void testBusinessLogic() {
// 模擬外部服務的行為
Mockito.when(externalService.getData()).thenReturn("Mock Data");
// 測試業務邏輯是否正確運作
String result = businessService.process();
assertEquals("Processed Mock Data", result);
}
}
@QuarkusTestResource
當在進行測試時,有時候需要依賴一些外部資源,比如資料庫、Kafka等,這些資源的管理對於測試的準確性和效率非常重要。在Quarkus中,@QuarkusTestResource
提供了便利的方式來管理這些外部資源,確保在測試的開始和結束時,這些資源可以自動啟動或停止。
例如使用 @QuarkusTestResource(H2DatabaseTestResource.class)
註冊了一個內存中的 H2資料庫 來進行測試。
```java
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.h2.H2DatabaseTestResource;
import org.junit.jupiter.api.Test;
@QuarkusTest
@QuarkusTestResource(H2DatabaseTestResource.class) // 設定測試時啟動H2資料庫
public class DatabaseTest {
@Test
public void testDatabaseOperation() {
// 測試資料庫操作
}
}
```
> H2DatabaseTestResource 用於 **本地測試環境** 的資料庫資源。它啟動的 **H2 資料庫** 是內存中的(in-memory),資料只會在測試過程中存在,測試結束後資料庫的所有資料都會被清除。
>
在前兩章節中,我快速介紹了AP層的重要技術概念,剩下的部分如容器部署、任務排程及緩存機制,偏向於Infra層面和系統設計。可惜的是,我沒有時間將這些部分完全完善。
如前面章節提到的,我原本打算好好撰寫Quarkus實作過程,但由於遇到組織內部問題,因此改寫了一些重要的後端議題。不過,我還是大致完整地介紹了Quarkus框架,包括GraalVM、響應式寫法等框架特性,也算是對Quarkus的應用有所交代。可惜的是,在雲端部分的撰寫時間只剩不到兩小時,雖然如此,我還是簡要介紹了Cloud SQL與Cloud Run。
現在讓我們來談談雲端,不知道沒使用過雲端的你,是否思考過雲端與本地系統運行的差異?比較常見的情況是,如果系統運行在本地,會有以下幾個考量點:
因此,這些限制會帶來一些問題:
以上這些問題,現有的公有雲服務都可以幫助解決,除了不籌基礎建設硬體建置與升級外,主要依據以下七大特點進行設計:
稍微整理一個簡單的比較表
特性 | 本地端部署 | 雲端部署 |
---|---|---|
資源配置 | 需自行管理硬體資源(伺服器、存儲裝置等),負責硬體更新與擴展 | 隨需存取 IT 資源,無需自行管理硬體 |
維運成本 | 需要監控伺服器、進行修復並進行軟體更新 | 由雲端服務商負責,降低維運成本 |
可用性與擴展性 | 受限於硬體資源上限,故障時需手動介入 | 彈性擴展,自動調整以應對變動的負載 |
開發與部署便利性 | 通常需要手動配置環境並部署應用程式 | 提供多種技術與服務,便於快速部署 |
應對變化需求 | 硬體規劃不易,可能導致資源閒置或不足 | 可根據需求快速擴充或縮減計算資源 |
應對間歇性需求 | 可能造成資源浪費或成本效益問題 | 按量付費模式,只需為實際消耗的資源支付費用 |
應對週期性需求 | 擴展彈性不足,需制定複雜的資源排程計劃 | 自動調整資源,無需複雜規劃 |
付費模式 | 需要大量前期投資 | 按量付費,無需預付費用或綁定長期合約 |
安全性 | 需自行建立和維護安全環境 | 提供高安全性,內建安全協議與專業技術團隊支援 |
全球可用性 | 需在各地部署本地 IT 資源 | 全球範圍內提供存取,便於進入新市場 |
技術與服務範圍 | 受限於自身技術能力 | 提供廣泛的技術與服務,無需專業知識也能輕鬆存取 |
成本效益 | 可能較高,尤其對中小型企業而言 | 享有規模經濟效益,通常更加經濟實惠 |
可以發現,雲端不只幫我們解決建設問題外,也同時幫我們解決安全性、多地區部屬與高可用性等問題! 那通常我們如何使用它? 這時就來小聊一下IaaS、PaaS、SaaS與on-site差異。如下圖所示
Cloud SQL與Cloud Run就屬於PaaS,讓開發者專注於應用程式開發和資料管理,無需關注基礎設施的維護與擴展。
Cloud SQL 是 Google Cloud 提供的完全託管 關聯式資料庫服務,支援常見的資料庫引擎,如 MySQL、PostgreSQL 和 SQL Server。負責管理備份、災難復原、高可用性及安全性,讓使用者專注於資料的管理與應用,而無需處理資料庫的運維工作。 除了根據選用硬體啟資料庫服務外,Cloud SQL 具體提供的功能還有
資料備份與還原 :
高可用性與故障轉移
資料加密與安全性
性能監控與洞察
資料庫監控與日誌
整合與支援
彈性擴展
自動更新與維護
MSSQL需要額外收授權費,很貴….
看完這些,你應該可以感受到雲端的強大,因為要實現這些功能,在基礎建設要下非常大的功夫….不知道你有沒有實做過架設postgres且需符合正規ssl連線。光是設置與ssl憑證管理就是很打的功夫了,這些在雲端只要幾個鍵就可以解決。
接著來介紹Cloud Run,接紹以前稍微提一下Serverless概念。
Serverless 是一種雲端運算模式,讓開發者不需要管理伺服器或基礎設施,能專注於應用程式的開發與執行。伺服器和基礎設施由雲服務提供商(如 Google Cloud)完全管理。Serverless主要特徵有
Cloud Run 是基於 Serverless 架構的容器執行平台,讓使用者能夠在不需管理伺服器的情況下直接運行容器化應用並具備一下特點
完全無伺服器架構
容器化應用支援
自動擴展
彈性計費
安全性與身份驗證
快速部署與整合
高可用性與冗餘
資料分析與監控
環境變數與密鑰管理
支援事件驅動架構
基本上要完成上述功能,一樣需要強大的基礎建設與框架。且部屬與設定都非常簡單,例如Quarkus 寫好了 Dockerfile,那麼只需要透過一個簡單的指令,就可以將這個容器化的應用部署到 Cloud Run。這邊稍微掩飾一下步驟
構建 Docker 映像檔:
將 Quarkus 應用程式打包成 Docker 映像檔。假設你已經在專案目錄中編寫好 Dockerfile,可以透過以下指令來構建 Docker 映像檔:
docker build -t [REGION]-docker.pkg.dev/[PROJECT_ID]/[REPOSITORY_NAME]/[IMAGE_NAME] .
[REGION]
所使用的區域,如 asia-east1
。[PROJECT_ID]
Google Cloud 中的專案 ID。[REPOSITORY_NAME]
GAR 中設定的倉儲名稱。[IMAGE_NAME]
Docker 映像檔的名稱。推送 Docker 映像檔到 Google Artifact Registry
docker push [REGION]-docker.pkg.dev/[PROJECT_ID]/[REPOSITORY_NAME]/[IMAGE_NAME]
使用 gcloud 指令部署到 Cloud Run
gcloud run deploy [SERVICE_NAME] --image [REGION]-docker.pkg.dev/[PROJECT_ID]/[REPOSITORY_NAME]/[IMAGE_NAME] --region [REGION] --platform managed
以上做完後,程式直接就Deploy到GCP服務上了,上面為一個簡單的掩飾。這邊沒時間了..不然還有牽扯到Load Balance設置與Cloud Armor安全設定。明年比鐵人會針對GCP雲端上手寫一個完整系列主題。
稍微總結一下,根據條例項目,可以感受到這些服務背後所需的基礎架構與技術支持相當龐大,若在傳統 IT 環境下自行搭建與維護,不論是硬體設備、資源配置還是安全保障,都將耗費大量資源與人力。而藉由雲端服務,我們可以更低的成本與更高的效率來實現這些目標,特別是針對需要彈性與擴展能力的應用程式,雲端技術具備極大的優勢。
雖然雲端很方便,但某種程度也讓工程師能力喪失,這點我非常認同! 如果有興趣可以參考此篇文章 淺談 Cloud Native 生態系下工程師逐漸喪失的那些技能
基本上這系列對於Quarkus實作雖然沒有完整呈現,但我相信過所有的章節過後,對於框架與雲端的開發底子會有一個程度很踏實的建制性。每一篇都非常認真去刁鑽,在這AI Chat現世的年代,我相信我所擰定的方向,在配合AI Chat會讓你有一個明確的方向且在能力建置上都有一個程度的提升!