iT邦幫忙

2024 iThome 鐵人賽

DAY 30
0
Software Development

微服務奇兵:30天Quarkus特訓營系列 第 30

開發-Quarkus急迫上手-Part3 : 附雲端簡談收尾

  • 分享至 

  • xImage
  •  

今天繼續聊上手Quarkus框架這件事情,然後再簡單聊一下雲端,並做一個收尾

依賴注入補充-@Produces

在依賴注入部分,沒有特別對@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;
    }
}

一般作法-直接使用 StreamingServiceProducer

在一般我們沒有使用@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();
    }
}

使用 @Produces 的解決方案

我們在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判定,這樣的寫法帶來許多好處

  1. 提升程式碼的可維護性和可讀性
    • 關注點分離@Produces 方法集中管理物件的建立邏輯,讓其他類別可以專注於使用這些物件,無需關心它們是如何被建立的。
    • 減少樣板程式碼:不需要在多個地方重複撰寫物件建立的邏輯,降低了程式碼重複。
  2. 改善相依性注入的使用體驗,簡化複雜相依性,對於需要複雜設定或初始化的物件,可以在 @Produces 方法中處理,簡化了使用方的程式碼。
  3. 提高測試效率和品質,在測試中,可以輕鬆替換 @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));
    }
}

Middleware如何使用

在Quarkus,大部分是使用 CDI(Contexts and Dependency Injection)來處理中介軟體(Middleware)等擴展功能。雖然沒有一個像 .NET Core Startup.cs 這樣的明確入口點,但你可以通過多種方式來實現插入中介軟體功能,具體做法如下:

1. 使用 JAX-RS Filters 和 Interceptors

攔截器允許在方法執行之前或之後,插入額外的邏輯操作,比如記錄日誌、驗證使用者是否有權限執行某個操作,或者對結果進行後處理。

定義攔截器

接著我們來透過 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 "操作已完成!";
    }
}

2. 過濾器(Filters)

過濾器是一個處理 HTTP 請求或回應的介面。可以在請求到達控制器之前,或回應發送給客戶端之前,進行額外的邏輯處理。這些處理通常包括驗證、記錄或修改請求/回應。

定義Request Filters

請求過濾器會攔截所有進入的 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 的組件,會自動註冊到框架中。

定義Response Filters

回應過濾器可以在 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 設定

啟用HTTPS,需要設定SSL憑證和私鑰。具體步驟如下:

  1. 需要有有效的SSL憑證和私鑰,檔案會以 .pem 格式存在。
  2. 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}

${} 語法表示這些值可以從環境變數中讀取

CORS 設定

允許應用程式接收來自不同網域的請求,

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)。

XSS(跨站腳本攻擊)防護

透過惡意腳本在用戶端執行來竊取資料。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是一種攻擊方式,攻擊者透過受害者的身份發起未經授權的操作。為防範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 設定允許的發行者,確保只有來自可信來源的令牌才會被接受。

Log套件使用

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 應用中記錄日誌。

  • JBoss Logging
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);
        }
    }
}
  • SLF4J
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

使用 JSON 日誌格式或檔案輸出

Quarkus 提供額外的日誌功能,如 JSON 格式的日誌輸出或將日誌寫入檔案。這些功能可以方便日後與其他監控系統整合,例如 ELK(Elasticsearch, Logstash, Kibana)或 Prometheus。

  • 啟用 JSON 格式日誌輸出
quarkus.log.console.json=true
  • 日誌寫入檔案
quarkus.log.file.enable=true
quarkus.log.file.path=logs/application.log
quarkus.log.file.level=INFO

配置 Quarkus 產生 JSON 格式日誌

透過修改 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 Mapper

可以使用 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();
    }
}

自定義 Mapper

// 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(/* 訂單詳情 */);
    }
}

MicroProfile 錯誤容忍

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 秒後恢復嘗試。

身分認證OpenId與 SAML使用

OpenID Connect (OIDC)

Quarkus 提供了內建的 quarkus-oidc 擴充套件,能夠讓應用程式使用 OIDC 協議進行使用者或服務的身份驗證。並且可以很方便地整合常見的身份提供者(如 Keycloak、Google 身份驗證等)。

  • 套件配置
dependencies {
    implementation enforcedPlatform("io.quarkus:quarkus-bom:3.0.0.Final")
    implementation 'io.quarkus:quarkus-oidc'
}
  • application.properties 中配置 OIDC
quarkus.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" 根據角色來源

  • Controller 中使用 @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();
    }
}

設定 SSO 步驟

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 流程。

SAML

Quarkus 沒有內建的 SAML 支援,可以透過 KeycloakPac4j 來實現 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() {
    
    }
}
  • 測試注入依賴
    Quarkus支援依賴注入測試,使用@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端點是否:

    1. 正確回應 狀態碼 200 (表示成功)。
    2. 回應的內容是 "Hello World"

    可以看到他符合測試的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。

On-premises vs Cloud

現在讓我們來談談雲端,不知道沒使用過雲端的你,是否思考過雲端與本地系統運行的差異?比較常見的情況是,如果系統運行在本地,會有以下幾個考量點:

  • 資源配置:組織需要自行管理硬體資源,例如伺服器、存儲裝置等,並負責硬體的更新與擴展,這些都需要額外的資金與時間投入。
  • 維運成本:需要監控伺服器、進行修復並進行軟體更新。
  • 可用性與擴展性設計:本地運行的系統如果發生硬體故障或災難,通常需要手動介入才能恢復。此外,本地系統的擴展性也受限於硬體資源的上限,當流量激增時,可能無法及時應對。
  • 開發與部署的便利性:本地部署通常需要手動配置環境並部署應用程式。

因此,這些限制會帶來一些問題:

  • 變化需求:硬體規劃不易,尖峰需求可能遠高於平均需求,導致大部分時間資源閒置;而在突發高峰期,資源可能無法快速擴展,造成系統延遲或當機。
  • 間歇性需求:在需求低谷期,大量硬體資源閒置,造成浪費。為了應對高峰需求,必須投入大量資金採購設備,但大部分時間設備無法完全利用,導致成本效益問題。
  • 週期性需求:擴展彈性不足,無法根據週期性變化靈活調整資源,可能導致某些時期資源過剩或不足。需要仔細分析需求週期,並制定複雜的資源排程計劃。

以上這些問題,現有的公有雲服務都可以幫助解決,除了不籌基礎建設硬體建置與升級外,主要依據以下七大特點進行設計:

  1. 隨需存取IT資源(On-demand access to IT resources):雲端資源如同使用電力,當需要時即可使用,不需要時則關閉,這樣能更有效率地管理資源。
  2. 按量付費模式(Pay-as-you-go pricing):使用者只需為實際消耗的資源支付費用,無需預付費用或綁定長期合約,計費靈活。
  3. 彈性擴展(Elastic):使用者可以根據需求隨時擴充或縮減計算資源,系統會自動調整以應對變動的負載,確保運行效率。
  4. 安全與可靠(Secure and reliable):雲端服務提供高安全性與高可靠性,透過內建的安全協議與專業技術團隊來保護資料,特別適合中小型企業,因為自行建立這類安全環境的成本過高。
  5. 全球可用性(Globally available):雲端服務在全球範圍內提供存取,使用者無需在各地部署本地IT資源,就能進入新市場,並且可以安全、可靠地使用雲端資源。
  6. 廣泛的技術與服務範圍(Broad range of technologies and services):雲端平台提供多種技術與服務,即使沒有專業知識的使用者也能輕鬆存取,讓企業更快速地部署解決方案。
  7. 規模經濟效益(Economies of scale):像GCP這類雲端服務提供商,擁有龐大的用戶基礎與規模經濟效益,能夠降低成本,並將這些效益回饋給所有客戶,使雲端服務更加經濟實惠。

稍微整理一個簡單的比較表

特性 本地端部署 雲端部署
資源配置 需自行管理硬體資源(伺服器、存儲裝置等),負責硬體更新與擴展 隨需存取 IT 資源,無需自行管理硬體
維運成本 需要監控伺服器、進行修復並進行軟體更新 由雲端服務商負責,降低維運成本
可用性與擴展性 受限於硬體資源上限,故障時需手動介入 彈性擴展,自動調整以應對變動的負載
開發與部署便利性 通常需要手動配置環境並部署應用程式 提供多種技術與服務,便於快速部署
應對變化需求 硬體規劃不易,可能導致資源閒置或不足 可根據需求快速擴充或縮減計算資源
應對間歇性需求 可能造成資源浪費或成本效益問題 按量付費模式,只需為實際消耗的資源支付費用
應對週期性需求 擴展彈性不足,需制定複雜的資源排程計劃 自動調整資源,無需複雜規劃
付費模式 需要大量前期投資 按量付費,無需預付費用或綁定長期合約
安全性 需自行建立和維護安全環境 提供高安全性,內建安全協議與專業技術團隊支援
全球可用性 需在各地部署本地 IT 資源 全球範圍內提供存取,便於進入新市場
技術與服務範圍 受限於自身技術能力 提供廣泛的技術與服務,無需專業知識也能輕鬆存取
成本效益 可能較高,尤其對中小型企業而言 享有規模經濟效益,通常更加經濟實惠

IaaS、PaaS、SaaS與on-site

可以發現,雲端不只幫我們解決建設問題外,也同時幫我們解決安全性、多地區部屬與高可用性等問題! 那通常我們如何使用它? 這時就來小聊一下IaaS、PaaS、SaaS與on-site差異。如下圖所示

https://ithelp.ithome.com.tw/upload/images/20241001/20115895dVNFatyGy2.png

  • On-premises(本地部署):傳統 IT 環境,所有管理責任都由企業自行負責,包含應用程式、資料、運行環境、伺服器、儲存、網路等層級,所有硬體與軟體皆需要自行設置與維護。
  • IaaS(基礎架構即服務):使用者可以租用雲端的基礎架構,例如虛擬機器和儲存資源,依需求自行管理作業系統與應用程式。
    • 使用者管理:應用程式、資料、執行環境、中介軟體、作業系統。
    • 服務供應商管理:虛擬化、伺服器、儲存、網路。
  • PaaS(平台即服務): 提供開發和部署應用程式的平台,使用者只需專注於應用程式和資料,底層的基礎設施和執行環境由服務供應商管理。
    • 使用者管理:應用程式、資料。
    • 服務供應商管理:執行環境、中介軟體、作業系統、虛擬化、伺服器、儲存、網路。
  • SaaS(軟體即服務): 服務供應商負責所有技術層級的管理,使用者只需使用應用程式,如 Google Workspace 或 Salesforce,無需管理基礎架構。
    • 服務供應商管理:應用程式、資料等所有層級。

Cloud SQL, Cloud Run

Cloud SQL與Cloud Run就屬於PaaS,讓開發者專注於應用程式開發和資料管理,無需關注基礎設施的維護與擴展。

Cloud SQL

Cloud SQL 是 Google Cloud 提供的完全託管 關聯式資料庫服務,支援常見的資料庫引擎,如 MySQL、PostgreSQL 和 SQL Server。負責管理備份、災難復原、高可用性及安全性,讓使用者專注於資料的管理與應用,而無需處理資料庫的運維工作。 除了根據選用硬體啟資料庫服務外,Cloud SQL 具體提供的功能還有

  1. 資料備份與還原 :

    • 自動備份:提供自動備份功能,確保資料持續受到保護。
    • 手動備份:使用者可以隨時進行手動備份,作為進一步的資料保護機制。
    • 還原:支援基於時間點的還原功能,方便快速復原到特定時間點的資料狀態
  2. 高可用性與故障轉移

    • 多區域高可用性:支援跨區域的高可用性部署,一旦資料庫故障,系統會自動將流量轉移到備援資料庫,以確保系統不中斷運行。
    • 自動故障轉移:當發生故障時,Cloud SQL 會自動進行故障轉移,確保業務連續性。
  3. 資料加密與安全性

    • 資料傳輸中的加密:所有資料在雲端傳輸時都會被加密,確保資料在網路中傳遞的安全性。
    • 靜態資料加密:資料儲存在 Cloud SQL 中時,也會被加密,以防止未經授權的存取。
    • IP 白名單:使用者可以限制特定 IP 位址存取資料庫,增強存取控制。
    • VPC 對等連線:透過 VPC 進行安全的私有網路連接,避免使用公網傳輸資料。
  4. 性能監控與洞察

    • SQL Insights:提供詳細的查詢分析功能,讓開發者能夠深入了解查詢的執行效率,找出慢查詢或資源使用率高的 SQL 語句,並進行優化。
    • 監控儀表板:Cloud SQL 提供圖形化的監控儀表板,讓使用者可以實時觀察資料庫的 CPU、內存、磁碟 I/O 和網路流量等指標,方便進行資源管理與性能調整。
    • 查詢性能剖析:幫助使用者深入分析查詢的執行路徑,發現性能瓶頸,並提供建議的索引與查詢優化方案。
  5. 資料庫監控與日誌

    • Stackdriver Logging:將操作日誌傳送到 Google Cloud Logging,方便用戶追蹤事件或排查錯誤。
    • Stackdriver Monitoring:結合 Google Cloud Monitoring,提供完整的監控方案,協助用戶即時查看資料庫的健康狀況,並設定警報提醒。
  6. 整合與支援

    • 與 Google Cloud 其他服務整合:可以與 Google Kubernetes Engine(GKE)、App Engine、Compute Engine 等服務無縫整合,支援各種應用程式部署。
    • 資料遷移工具:提供簡易的遷移工具,支援從本地或其他雲端服務平臺遷移資料至 Cloud SQL。
  7. 彈性擴展

    • 垂直擴展:支援動態調整 CPU、內存等資源,根據工作負載需求進行資源擴充或縮減。
    • 水平擴展:支援讀寫分離和多讀副本,能分散讀取請求,提升整體讀取性能。
  8. 自動更新與維護

    • 提供自動化的作業系統更新與安全修補,確保使用者的資料庫環境始終保持最新,並抵禦潛在的安全威脅。

MSSQL需要額外收授權費,很貴….

看完這些,你應該可以感受到雲端的強大,因為要實現這些功能,在基礎建設要下非常大的功夫….不知道你有沒有實做過架設postgres且需符合正規ssl連線。光是設置與ssl憑證管理就是很打的功夫了,這些在雲端只要幾個鍵就可以解決。

Cloud Run

接著來介紹Cloud Run,接紹以前稍微提一下Serverless概念。

Serverless

Serverless 是一種雲端運算模式,讓開發者不需要管理伺服器或基礎設施,能專注於應用程式的開發與執行。伺服器和基礎設施由雲服務提供商(如 Google Cloud)完全管理。Serverless主要特徵有

  1. 自動擴展 : 應用程式根據流量自動調整資源。
  2. 按使用量計費 : 使用者只為實際使用的資源付費,例如 Cloud Run 是根據處理請求的時間進行計費,當沒有請求時,則不會產生成本。
  3. 免維護基礎設施 : 雲端提供商負責伺服器的運行、維護、修補更新、資源管理等工作,使用者無需擔心伺服器設定與管理。
  4. 高可用性與容錯性 : 自動處理伺服器故障、備份、恢復等工作,應用程式可以跨多個地區部署,具備更好的冗餘和容錯性。

Cloud Run 是基於 Serverless 架構的容器執行平台,讓使用者能夠在不需管理伺服器的情況下直接運行容器化應用並具備一下特點

  1. 完全無伺服器架構

    • 無需管理基礎設施:使用者不必管理伺服器、儲存、網路等,這些由 Google Cloud 完全管理。
    • 按需自動調整:根據流量動態自動擴展與縮減資源,確保應用程式能夠彈性運行。當沒有請求時,Cloud Run 甚至可以將資源縮減到零,避免不必要的成本浪費。
    • 無狀態工作負載:Cloud Run 特別適合無狀態應用程式或事件驅動的工作負載,運行短期、高效的處理任務。
  2. 容器化應用支援

    • 多語言支援:無論是 Java、Python、Go、Node.js 還是其他語言,只要應用程式包裝成容器,Cloud Run 都能運行。
    • 開發靈活性:開發者可以自由選擇開發框架、依賴庫和工具,無需依賴特定的技術棧,只需將應用程式封裝成容器(例如 Docker)。
    • 標準容器支援:Cloud Run 完全支援 OCI(Open Container Initiative)標準的容器映像檔,讓使用者能輕鬆在多種平台之間遷移容器。
  3. 自動擴展

    • 基於流量的自動擴展:Cloud Run 會自動根據實際的流量負載來擴展運算資源,確保應用程式在高負載時仍然能夠穩定運行,並在流量降低時縮減資源以節省成本。
    • 每個容器的自動啟動:根據請求的到來,自動啟動並擴展多個容器實例以應對並發請求,且能動態分配資源。
  4. 彈性計費

    • 按使用量付費:Cloud Run 根據實際的使用量進行計費,使用者只需為應用程式處理請求的時間和資源支付費用,當沒有流量時不會產生成本。
    • 細緻的資源設定:使用者可以為每個容器實例設置 CPU、記憶體等資源,依需求靈活調整,進一步控制成本。
  5. 安全性與身份驗證

    • TLS/SSL 加密:所有通過 Cloud Run 的 HTTP 流量都自動使用 HTTPS 進行加密,確保資料在傳輸過程中的安全性。
    • Google Cloud IAM(身份與存取管理):Cloud Run 可以與 Google Cloud IAM 整合,實現細緻的權限管理,控制誰可以存取和部署應用程式。
    • 私有服務支援:支援 VPC(虛擬私有雲)網路,允許 Cloud Run 應用程式與私有網路內的服務安全地進行通訊。
  6. 快速部署與整合

    • 簡單部署:只需一條命令即可將容器部署至 Cloud Run,簡化了傳統基礎架構設置的複雜性。
    • 支援持續整合與持續部署(CI/CD):Cloud Run 無縫整合 Google Cloud Build,允許使用者在每次程式碼變更後自動構建並部署容器,支持 CI/CD 流程。
    • 與 GKE 無縫整合:如果需要更精細的控制,Cloud Run 容器可以輕鬆遷移到 Google Kubernetes Engine(GKE)中運行,兩者相互兼容。
  7. 高可用性與冗餘

    • 多區域部署:Cloud Run 支援跨區域部署,使用者可以在全球多個地區中選擇部署位置,確保應用程式的高可用性。
    • 冗餘與故障恢復:由於是完全託管的服務,Cloud Run 在基礎設施層級提供了內建的冗餘與自動故障恢復功能,確保應用程式不會因基礎設施故障而中斷。
  8. 資料分析與監控

    • Google Cloud Operations(原 Stackdriver)整合:Cloud Run 可以與 Google Cloud Monitoring 和 Logging 整合,使用者可以在單一介面上監控應用程式的執行狀態,追蹤流量、錯誤率、延遲等指標,並分析日誌。
    • 自訂監控:使用者可以自訂各類監控指標,針對特定的性能瓶頸、資源消耗等問題進行調整與優化。
    • 告警功能:結合監控系統,使用者可以設定告警規則,當應用程式發生異常狀況(如資源不足、流量高峰)時即時通知。
  9. 環境變數與密鑰管理

    • 環境變數管理:Cloud Run 允許使用者設置應用程式的環境變數,便於根據不同環境(如開發、測試、正式環境)來調整設定。
    • Google Cloud Secret Manager 整合:使用者可以安全地儲存和管理應用程式所需的敏感資訊(如 API 金鑰、憑證),並將其與 Cloud Run 整合,無需硬編碼在程式中。
  10. 支援事件驅動架構

    • 與 Cloud Pub/Sub 整合:Cloud Run 支援事件驅動的架構,可以與 Google Cloud Pub/Sub 整合,讓應用程式根據特定的事件(例如訊息的到達)自動觸發,並動態調整資源。
    • 與 Cloud Functions 整合:Cloud Run 能夠和 Cloud Functions 結合,適合需要使用伺服器功能來觸發長期運行的背景任務或處理批次任務的場景。

基本上要完成上述功能,一樣需要強大的基礎建設與框架。且部屬與設定都非常簡單,例如Quarkus 寫好了 Dockerfile,那麼只需要透過一個簡單的指令,就可以將這個容器化的應用部署到 Cloud Run。這邊稍微掩飾一下步驟

  1. 構建 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 映像檔的名稱。
  2. 推送 Docker 映像檔到 Google Artifact Registry

    docker push [REGION]-docker.pkg.dev/[PROJECT_ID]/[REPOSITORY_NAME]/[IMAGE_NAME]
    
  3. 使用 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 生態系下工程師逐漸喪失的那些技能

30天總結

基本上這系列對於Quarkus實作雖然沒有完整呈現,但我相信過所有的章節過後,對於框架與雲端的開發底子會有一個程度很踏實的建制性。每一篇都非常認真去刁鑽,在這AI Chat現世的年代,我相信我所擰定的方向,在配合AI Chat會讓你有一個明確的方向且在能力建置上都有一個程度的提升!


上一篇
開發-Quarkus急迫上手-Part2
系列文
微服務奇兵:30天Quarkus特訓營30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言