iT邦幫忙

2024 iThome 鐵人賽

DAY 28
0

今天來談如何使用Quarkus,其實這系列原本是因為要接公司接下來的專案而寫,本來這系列是想寫給跟相關案子的人可以快速上手這Quarkus框架,不過遇到一些公司湯圓(專案可能後續有機會不用維護),所以對於Quarkus的興趣就興致缺缺了(感覺還是java spring flux大宗?)。

我心態這樣,文章不會得名阿!?? 這次參加也不是拿名次為主,就真的只是為了案子….可惜。不過也因此重新過了許多重要觀念,是真的很認真寫每一個章節,算因獲得福??????

框架上手需學項目

不過還是針對以前的經驗稍微過一下我覺得比較重要的部分,我認為上手框架在Application面有下列項目要理解

  1. REST API撰寫 : 如何撰寫Controller層,一般針對框架或是套件會有很多Attribute上的設置。通常會涉及許多框架或套件提供的註解(Attributes)設置,用來定義HTTP請求的路由、方法和參數等。
  2. DB連線 : 如何使用框架做資料庫連線,包括如何設置SSL加密連線,確保數據傳輸的安全。
  3. 依賴注入(DI框架): 基本上現行架構都會帶DI設置,在系統設計上是很重要的環節,除了關係到架構設計的影響外,還與物件是否有效管理生命週期有關。
  4. 環境配置管理 : 根據不同環境(如SIT、UAT、Prod)注入對應的配置變數,保證應用能夠適應不同的運行環境。
  5. Middleware如何使用 : 負責應用之間的溝通協調,第一層可以實現身份驗證、安全驗證與冪等性檢查等功能。
  6. 安全性設置:包括HTTPS、CORS、XSS防護和CSRF防護,確保應用的資料傳輸安全,並防止常見的網路攻擊。
  7. Log套件使用 : 記錄應用程式的運行狀況,包含正常運作記錄和錯誤發生時的資訊,讓開發者進行調試與監控。
  8. 錯誤處理與異常管理 : 當應用程式遇到錯誤或意外情況時,錯誤處理機制會捕捉並提供有意義的錯誤訊息,而不是讓應用 Crash,這通常涉及到Error Handling的設計。
  9. AOP套件或框架使用 :將如日誌記錄、交易管理等橫切關注點與業務邏輯分離,使系統更具可維護性和靈活性。
  10. 身分認證OpenId與 SAML使用 : 透過OpenID或SAML協議進行身份認證,常用於用戶登入和安全性確認。
  11. Object-Object Mapper : 資料物件與不同格式(如 JSON)互相轉換的工具。
  12. 單元測試 : 驗證應用程式內部每個功能是否按預期邏輯處理。
  13. 容器化和部署 : 將應用打包成一個可獨立運行的服務單元,這個服務單元可以在任何支持容器的平台上運行。
  14. 任務排程 : 允許應用程式定期執行特定任務,如定期產生報告或進行資料備份。
  15. 緩存機制 : 將常用的資料存儲在記憶體中,減少資料庫的頻繁訪問,從而加速應用的響應時間。

以上為熟悉框架常見須了解的項目,其餘的話則就是針對框架特性,如前些章節提到的Native Image與適合響應寫法這件事情,這邊就不在多說了。接著我們針對每個項目快速過一遍(個人覺得Quarkus官方文件不太友善…),基本上這邏輯在大都框架都適用。

REST API撰寫

Quarkus 中如何撰寫 REST API,基本上Quarkus在這部分是使用RESTEasy API去實作。使用 JAX-RS 註解(由 RESTEasy 支援)來定義路由、請求方法、輸出輸入格式等。

  • @Path:定義 API 的路徑。
  • @GET、@POST、@PUT、@DELETE:定義 HTTP 方法。
  • @Produces、@Consumes:定義輸入和輸出的格式,如 JSON。
  • @PathParam、@QueryParam、@HeaderParam:提取路徑、查詢或標頭中的參數。
  • @DefaultValue:為查詢參數或標頭參數設置預設值。
  • @FormParam:提取表單參數。

JAX-RS 註解就是讓開發者可以用簡單的標註方式(Attribute),直接在 Java 程式碼中定義 RESTful API 的各種設定,包括路由、HTTP 方法、參數處理等,符合 REST 規範和 HTTP 協定,不用寫一堆繁瑣的設定檔,整個開發過程變得更有效率。

1. 使用 @Path 註解定義路徑

@Path 是用來指定 API 的路徑,也就是客戶端發送 HTTP 請求時訪問的 URL。例如,我們要提供一個 /notifications 的 API:

@Path("/notifications")
public class NotificationResource {
    // 這個類所有的路由都會以 /notifications 開頭
}

如果我們有一個 /notifications/{id} 的 API,則可以使用路徑參數:

@GET
@Path("/{id}")
public Notification getNotification(@PathParam("id") Long id) {
    // 根據 ID 取得推播訊息
    return notificationService.findById(id);
}
  • @Path("/{id}"){} 用來代表路徑中的參數部分。
  • @PathParam("id"):提取 URL 中的 id 值,並作為參數傳入方法。

2. 使用 @GET@POST@PUT@DELETE 註解定義 HTTP 方法

  • @GET:用來處理查詢(GET)請求,取得資料。
  • @POST:用來處理新增(POST)請求,新增資料。
  • @PUT:用來處理更新(PUT)請求,更新資料。
  • @DELETE:用來處理刪除(DELETE)請求,刪除資料。

例如,

@GET
public List<Notification> listNotifications() {
    return notificationService.getAll();
}

@POST
public Response createNotification(Notification notification) {
    notificationService.add(notification);
    return Response.status(Response.Status.CREATED).build();
}

@PUT
@Path("/{id}")
public Response updateNotification(@PathParam("id") Long id, Notification notification) {
    notificationService.update(id, notification);
    return Response.ok().build();
}

@DELETE
@Path("/{id}")
public Response deleteNotification(@PathParam("id") Long id) {
    notificationService.delete(id);
    return Response.noContent().build();
}
  • @GET:用來查詢推播訊息,回傳 List<Notification>
  • @POST:新增推播訊息,並回傳 201 Created
  • @PUT:更新某個指定的推播訊息(根據 ID)。
  • @DELETE:刪除某個指定的推播訊息(根據 ID)。

3. 使用 @Produces@Consumes 註解定義資料格式

用來指定 API 返回的資料格式API 接收的資料格式。常用的格式有:

  • MediaType.APPLICATION_JSON:JSON 格式,最常見的 Web API 格式。
  • MediaType.TEXT_PLAIN:純文本格式。

例如

@GET
@Produces(MediaType.APPLICATION_JSON)  // 回傳 JSON 格式
public List<Notification> listNotifications() {
    return notificationService.getAll();
}

@POST
@Consumes(MediaType.APPLICATION_JSON)  // 接收 JSON 格式
@Produces(MediaType.APPLICATION_JSON)  // 回傳 JSON 格式
public Response createNotification(Notification notification) {
    notificationService.add(notification);
    return Response.status(Response.Status.CREATED).entity(notification).build();
}
  • @Produces(MediaType.APPLICATION_JSON):指定 API 的回應是 JSON 格式。
  • @Consumes(MediaType.APPLICATION_JSON):指定 API 接收的是 JSON 格式。

4. 使用 @QueryParam@HeaderParam@PathParam 提取參數

  • @PathParam:用來提取 URL 路徑中的參數,如 /notifications/{id}
  • @QueryParam:用來提取 URL 中的查詢參數,如 /notifications?type=new
  • @HeaderParam:用來提取 HTTP 標頭中的參數。

例如

@GET
@Path("/filter")
public List<Notification> getNotificationsByType(@QueryParam("type") String type) {
    return notificationService.findByType(type);
}

@GET
@Path("/{id}")
public Notification getNotificationById(@PathParam("id") Long id) {
    return notificationService.findById(id);
}

@POST
@Path("/secure")
public Response createSecureNotification(@HeaderParam("Authorization") String token, Notification notification) {
    if (authService.validateToken(token)) {
        notificationService.add(notification);
        return Response.status(Response.Status.CREATED).build();
    } else {
        return Response.status(Response.Status.UNAUTHORIZED).build();
    }
}

  • @QueryParam("type"):提取查詢參數,如 /filter?type=new
  • @HeaderParam("Authorization"):提取 HTTP 標頭中的 Authorization 欄位,用來進行身分驗證。

5. 使用 @DefaultValue 設定預設值

當查詢參數或路徑參數沒有提供時,可以使用 @DefaultValue 註解來提供預設值。

例如

@GET
@Path("/list")
public List<Notification> listNotifications(@QueryParam("page") @DefaultValue("1") int page,
                                            @QueryParam("size") @DefaultValue("10") int size) {
    return notificationService.getNotifications(page, size);
}

這裡 pagesize 參數在未提供時會自動設為 110

6. Quarkus 中實現 API 版本控制的方式

  • URL 路徑版本控制(Google建議作法):在 URL 路徑中添加版本號,如 /v1/v2。這樣不同的客戶端可以根據版本號訪問不同的 API。範例如下
// API 版本 1
@Path("/v1/notifications")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class NotificationResourceV1 {

    @GET
    public List<Notification> getAllNotificationsV1() {
        // 這裡返回的可能是舊版的資料格式
        return notificationService.getAllNotificationsV1();
    }
}

// API 版本 2
@Path("/v2/notifications")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class NotificationResourceV2 {

    @GET
    public List<Notification> getAllNotificationsV2() {
        // 這裡可能返回更豐富的資料或不同的格式
        return notificationService.getAllNotificationsV2();
    }

    @POST
    public Response createNotificationV2(Notification notification) {
        notificationService.createNotificationV2(notification);
        return Response.status(Response.Status.CREATED).build();
    }
}

  • HTTP 標頭版本控制:通過 HTTP 請求的標頭來指定版本號。
@Path("/notifications")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class NotificationResource {

    @GET
    public Response getAllNotifications(@HeaderParam("X-API-Version") String apiVersion) {
        if ("v1".equals(apiVersion)) {
            return Response.ok(notificationService.getAllNotificationsV1()).build();
        } else if ("v2".equals(apiVersion)) {
            return Response.ok(notificationService.getAllNotificationsV2()).build();
        } else {
            return Response.status(Response.Status.BAD_REQUEST)
                .entity("Unsupported API version: " + apiVersion).build();
        }
    }
}

  • 查詢參數版本控制:使用查詢參數來指定 API 版本號(不推薦,就不給範例了)。

另外需注意的是,RESTEasy 不只支持傳統的阻塞式 REST API,支持響應式(Reactive) API 實作。套件選用上會有些不同

  • RESTEasy Classic:傳統的阻塞式 API,適合於同步請求和響應的處理方式,這是大多數 REST API 開發中常用的形式。
  • RESTEasy Reactive:非阻塞式 API,適合高併發場景,能有效處理大量並發請求,提供更好的資源利用率。

DB連線

這部分通常使用 Hibernate ORM with Panache 來簡化與資料庫的連線互動。大致的步驟為

  1. 安裝 Quarkus 的資料庫套件
  2. 配置資料庫連線,包括基本連線設定和 SSL 加密連線。
  3. 創建資料庫實體(Entities)。
  4. 使用 Panache 進行簡化的 CRUD 操作。

1. 安裝 Quarkus 的資料庫套件

build.gradle 文件中添加相應的依賴來使用 Quarkus 與 PostgreSQL 資料庫。

dependencies {
    // Quarkus Hibernate ORM with Panache - 用來簡化 JPA 操作
    implementation 'io.quarkus:quarkus-hibernate-orm-panache'

    // PostgreSQL JDBC Driver - 連接 PostgreSQL 資料庫
    implementation 'io.quarkus:quarkus-jdbc-postgresql'

    // 其他 Quarkus 套件...
}

2. 配置資料庫連線,包括基本連線設定和 SSL 加密連線。

使用 application.properties 文件來進行資料庫連線的配置。

基本資料庫連線設定

# 資料庫類型
quarkus.datasource.db-kind=postgresql

# 資料庫連線資訊
quarkus.datasource.username=your_db_username
quarkus.datasource.password=your_db_password
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/your_db_name

# 啟用 Hibernate 的 DDL 自動生成功能 (none, create, update, drop-and-create)
quarkus.hibernate-orm.database.generation=update

SSL 加密連線設定

# PostgreSQL 的 SSL 加密連線
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/your_db_name?ssl=true&sslmode=require
  • ssl=true:啟用 SSL 。
  • sslmode=require:要求所有連線都必須使用 SSL,確保資料傳輸的安全性。

3. 創建資料庫實體(Entities)

創建一個對應到資料庫表的 實體類(Entity),以便進行 CRUD 操作。Quarkus 提供了 Panache,能夠簡化 JPA 操作。假設我們有一個 Notification 表,用來存放推播訊息,我們可以創建對應的實體類:

import javax.persistence.Entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;

@Entity
public class Notification extends PanacheEntity {

    public String title;
    public String message;
    public String timestamp;

    // PanacheEntity 已經自動提供了 ID 欄位及常用的 CRUD 方法
}
  • @Entity:標示這是一個 JPA 實體類,會對應到資料庫中的Table。
  • PanacheEntity:繼承自 Quarkus 提供的 Panache 類,內建了 id 屬性(自動生成),並提供常見的 CRUD 方法(如 persist()delete())。

4. 使用 Panache 進行簡化的 CRUD 操作

不需要單獨創建 Repository 類,就能直接進行 CRUD 操作。範例如下

import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import java.util.List;

@Path("/notifications")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class NotificationResource {

    // 查詢所有推播訊息
    @GET
    public List<Notification> listAll() {
        return Notification.listAll();  // Panache 提供的 listAll 方法
    }

    // 查詢單一推播訊息
    @GET
    @Path("/{id}")
    public Notification getNotification(@PathParam("id") Long id) {
        return Notification.findById(id);  // 根據 ID 查詢
    }

    // 新增推播訊息
    @POST
    public Notification createNotification(Notification notification) {
        notification.persist();  // 新增資料到資料庫
        return notification;
    }

    // 更新推播訊息
    @PUT
    @Path("/{id}")
    public Notification updateNotification(@PathParam("id") Long id, Notification notification) {
        Notification existingNotification = Notification.findById(id);
        if (existingNotification == null) {
            throw new WebApplicationException("Notification with ID " + id + " does not exist.", 404);
        }
        existingNotification.title = notification.title;
        existingNotification.message = notification.message;
        existingNotification.timestamp = notification.timestamp;
        return existingNotification;
    }

    // 刪除推播訊息
    @DELETE
    @Path("/{id}")
    public void deleteNotification(@PathParam("id") Long id) {
        Notification.deleteById(id);  // 根據 ID 刪除
    }
}

  • listAll():查詢所有資料。
  • findById(id):根據 ID 查詢資料。
  • persist():插入或更新資料。
  • deleteById(id):根據 ID 刪除資料。

我們使用 Notification.listAll() 簡單地查詢所有推播訊息。使用 persist() 方法將新的推播訊息存入資料庫。使用 deleteById() 方法刪除推播訊息,整體 CRUD 操作變得非常簡單直觀。

5. 使用 Repository Interface 抽換 Data Source

雖然不用實作Repository,不過大多狀態下一般會去設計Repository介面來抽換Data Source。

建置 Repository Interface

import io.quarkus.hibernate.orm.panache.PanacheRepository;
import javax.enterprise.context.ApplicationScoped;
import java.util.List;

@ApplicationScoped
public class NotificationRepository implements PanacheRepository<Notification> {
    // PanacheRepository 提供了基本的 CRUD 方法,也可以自定義查詢
    public List<Notification> findByTitle(String title) {
        return list("title", title);
    }
}

使用 Repository Interface

NotificationResource 中,使用 依賴注入(DI) 來注入 NotificationRepository。這樣即使將來更換資料來源,也只需要修改 Repository 的實作,而不影響 API 層。

import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import java.util.List;

@Path("/notifications")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class NotificationResource {

    @Inject
    NotificationRepository notificationRepository;  // 注入 Repository

    @GET
    public List<Notification> listAll() {
        return notificationRepository.listAll();  // 使用 Repository 來查詢資料
    }

    @GET
    @Path("/{id}")
    public Notification getNotification(@PathParam("id") Long id) {
        return notificationRepository.findById(id);  // 根據 ID 查詢
    }

    @POST
    public Notification createNotification(Notification notification) {
        notificationRepository.persist(notification);  // 新增推播訊息
        return notification;
    }

    @DELETE
    @Path("/{id}")
    public void deleteNotification(@PathParam("id") Long id) {
        notificationRepository.deleteById(id);  // 刪除推播訊息
    }
}

這樣設計的好處是,如果將來需要更換資料來源,只需實作新的 NotificationRepository,並使用不同的資料存取方式即可,對上層 API 邏輯沒有影響。

6. 使用環境變數來管理 DB 帳號密碼

在實際部署中,將資料庫帳號密碼寫死在 application.properties 是不安全的。通常會透過環境變數來動態配置這些敏感資訊。

使用環境變數設置資料庫帳號密碼

可以將資料庫的連線資訊(如帳號、密碼等)透過環境變數傳入,而不是直接寫在 application.properties 中。例如:

# 透過環境變數來讀取資料庫的帳號、密碼
quarkus.datasource.username=${DB_USERNAME:default_username}
quarkus.datasource.password=${DB_PASSWORD:default_password}

# 其他連線設定
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/your_db_name
quarkus.datasource.db-kind=postgresql
quarkus.hibernate-orm.database.generation=update
  • DB_USERNAMEDB_PASSWORD:環境變數。如果環境變數沒有設置,會使用 default_usernamedefault_password 作為預設值。
  • quarkus.datasource.usernamequarkus.datasource.password:可以直接從環境變數中取得資料庫帳號和密碼,這樣在不同的環境下,可以動態改變。

一般在K8環境會使用ConfigMap 搭配 Secret來做變數注入,而GCP上也是如此,通常實際帳號密碼會放在Secret,再由ConfigMap去吃不同環境Yaml去注入。

明天繼續!


上一篇
開發概念建置- 微服務監控設計大小事
下一篇
開發-Quarkus急迫上手-Part2
系列文
微服務奇兵:30天Quarkus特訓營30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
pal2097
iT邦新手 5 級 ‧ 2024-10-07 21:45:55

框架上手補充(包含語言特性)

  • DB ORM
  • DB Code First Package
  • grpc
  • 語言Naming Sytle
  • Project layout
  • ci lint
  • rfc7807實現方式

我要留言

立即登入留言