iT邦幫忙

2024 iThome 鐵人賽

DAY 29
0

今天繼續聊上手Quarkus框架這件事情,

依賴注入

Quarkus的Dependency Injection是透過Contexts and Dependency框架來實作。依賴注入容器主要用來管理物件相依性,能自動實體化類別,並透過容器來判斷物件生命週期。注入模式分別有

注入方式

建構子注入 (Constructor Injection)

物件創建時通過建構子來提供該物件所需的依賴。當 ServiceA 被創建時,ServiceBServiceC通過建構子被注入。

@ApplicationScoped
public class ServiceA {
    private final ServiceB serviceB;
    private final ServiceC serviceC;

    @Inject
    public ServiceA(ServiceB serviceB, ServiceC serviceC) {
        this.serviceB = serviceB;
        this.serviceC = serviceC;
    }

    // ... methods using serviceB and serviceC
}

@ApplicationScoped
public class ServiceB {
    // ... implementation
}

@ApplicationScoped
public class ServiceC {
    // ... implementation
}

屬性注入 (Property Injection)

使用 @Inject 直接在類別屬性上進行注入,屬性注入通常較少使用,因為它允許相依關係在物件建立後被變更(可以加final,不過通常還是建議使用建構子注入)。

import javax.inject.Inject;
import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class ServiceA {

    @Inject
    private ServiceB serviceB;

    public String serve() {
        return serviceB.doSomething();
    }
}

方法注入 (Method Injection)

較為少見的注入方式,常用於需要在方法層級進行依賴注入的場景。

import javax.inject.Inject;
import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class ServiceA {

    private ServiceB serviceB;

    // Method Injection
    @Inject
    public void setServiceB(ServiceB serviceB) {
        this.serviceB = serviceB;
    }

    public String serve() {
        return serviceB.doSomething();
    }
}

多個相同類型的 Bean注入

假設有兩個類型相同的實現,但它們的行為不同,我們可以為每個 Bean 使用不同的 @Qualifier 來區分它們。

  1. 定義一個新的 @Qualifier 註解,使用 @Qualifier@Retention(RetentionPolicy.RUNTIME),確保這個註解在運行時存在,並可以用來標記不同的 Bean。
import javax.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCustomQualifier {
    String value();
}

@Retention 指定註解的生命週期,即註解應該在什麼時候被保留,從編譯時期到執行時期。

  • RetentionPolicy.SOURCE:註解只存在於原始碼中,編譯器會在編譯時期將其丟棄(VM 在執行程式時,也無法讀取這些註解)。
  • RetentionPolicy.CLASS:註解會保留在編譯後的 .class 檔案中,但 JVM 在執行時不會加載它。
  • RetentionPolicy.RUNTIME:註解會一直保留到執行時,並且可以透過 Java 反射機制來讀取這些註解。
  1. 定義兩個 Bean接著,我們定義兩個實現,並使用 @Qualifier 來區分它們:
import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
@MyCustomQualifier("beanA")
public class MyBeanA implements MyService {
    @Override
    public void execute() {
        System.out.println("Executing Bean A");
    }
}

@ApplicationScoped
@MyCustomQualifier("beanB")
public class MyBeanB implements MyService {
    @Override
    public void execute() {
        System.out.println("Executing Bean B");
    }
}

此範例為 MyBeanAMyBeanB 分別定義了 @MyCustomQualifier("beanA")@MyCustomQualifier("beanB") 來區分這兩個不同的實現。

  1. 使用 @Qualifier 注入特定的 Bean 最後,可以在需要的地方,透過指定 @Qualifier 來注入特定的 Bean:
import javax.inject.Inject;

public class MyServiceClient {

    @Inject
    @MyCustomQualifier("beanA")
    MyService service;

    public void performTask() {
        service.execute();  // 這裡會調用 MyBeanA 的 execute 方法
    }
}

MyServiceClient 類別將會注入 MyBeanA,因為使用了 @MyCustomQualifier("beanA")

注入的生命週期

依賴注入的生命週期是透過 CDI 的範疇 (Scopes) 來管理的。簡單來說,範疇決定了物件被注入後,可以存活多久,也就是這個物件會在什麼時候被建立、何時會被銷毀。

@ApplicationScoped

最常見的範疇,Bean 的實例在應用的整個生命週期內持續存在,且在應用範圍內是單例的。

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class ApplicationService {
    public String serve() {
        return "Application scoped service";
    }
}

@RequestScoped

Bean 的生命週期與 HTTP 請求同步,請求結束時,Bean 也會被銷毀。

import javax.enterprise.context.RequestScoped;

@RequestScoped
public class RequestService {
    public String processRequest() {
        return "Processing request";
    }
}

@SessionScoped

Bean 的生命週期與 HTTP Session 相同,在 session 結束時銷毀。

import javax.enterprise.context.SessionScoped;
import java.io.Serializable;

@SessionScoped
public class SessionService implements Serializable {
    public String getSessionId() {
        return "Session scoped service";
    }
}

@Dependent

Quarkus 中最短的範疇,每次注入或每次被呼叫時,Bean 都會被創建。

import javax.enterprise.context.Dependent;

@Dependent
public class DependentService {
    public String getDependentMessage() {
        return "I am a new instance every time!";
    }
}

因為我擅長Net Core,這邊順手整理一下與Net Core的DI方法對應

Quarkus 範疇 .NET Core 生命週期 說明
@ApplicationScoped Singleton 在應用程式生命週期內只建立一次的單例,適合共享狀態或配置管理。
@RequestScoped Scoped 針對每個 HTTP 請求建立一次,請求結束時銷毀,適用於處理每個請求都需要不同狀態的情境。
@SessionScoped Session 針對每個使用者的 Session 建立一次,並在 Session 結束時銷毀,通常用於需要保持使用者狀態的服務(如認證)。
@Dependent Transient 每次注入或每次使用時,Bean 都會重新建立,適合不需要保存狀態的短期物件。

環境配置管理

根據不同環境注入對應的設定變數。以下是在 Quarkus 中實現環境設定管理的具體方法:

1. 建立設定檔:

首先,為每個環境建立一個設定檔。通常,會有以下檔案:

src/main/resources/application.properties (預設設定)
src/main/resources/application-sit.properties (SIT 環境)
src/main/resources/application-uat.properties (UAT 環境) 
src/main/resources/application-prod.properties (正式環境)

2. 設定環境特定的參數:

在每個環境特定的設定檔中,加入該環境所需的設定。例如,在 application-sit.properties 中:

quarkus.datasource.jdbc.url=jdbc:postgresql://sit-db-server/mydb
quarkus.http.port=8080
custom.api.url=http://sit-api.example.com

3. 啟用設定:

有幾種方式可以啟用特定環境的設定:

a. 使用系統屬性:

java -Dquarkus.profile=sit -jar quarkus-app.jar

b. 使用環境變數:

export QUARKUS_PROFILE=sit
java -jar quarkus-app.jar

c. 在 application.properties 中設定預設 profile:

quarkus.profile=sit

4. 在程式碼中使用設定:

使用 @ConfigProperty 註解來注入設定值:

import org.eclipse.microprofile.config.inject.ConfigProperty;

@Path("/hello")
public class GreetingResource {

    @ConfigProperty(name = "custom.api.url")
    String apiUrl;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello from " + apiUrl;
    }
}

5. 設定優先順序:

Quarkus 遵循一定的優先順序來解析設定:

  • 系統屬性
  • 環境變數
  • 環境特定的設定檔(如 application-sit.properties
  • 預設設定檔(application.properties

可以透過系統屬性或環境變數覆寫任何設定值,無需更改設定檔。

6. 使用設定群組:

對於更複雜的設定,可以使用設定群組:

@ConfigMapping(prefix = "myapp.service")
public interface ServiceConfig {
    String url();
    int port();
}

@Inject
ServiceConfig serviceConfig;

Object-Object Mapper

進行資料物件與 JSON 等不同格式的轉換,可以使用 JSON-B (JSON Binding)Jackson 這兩個常用工具,以下說明如何在 Quarkus 中實作物件與格式的相互轉換:

使用 JSON-B

Quarkus 預設支援 JSON-B,可以簡單地將 Java 物件序列化(轉換成 JSON)或反序列化(從 JSON 轉換為物件)。以下是範例:

步驟 1:新增相依套件

./mvnw quarkus:add-extension -Dextensions="jsonb"

步驟 2:建立 POJO(Plain Old Java Object)

public class User {
    public String name;
    public int age;

    // 可選擇性地加入建構子、getter 和 setter
}

步驟 3:序列化與反序列化

Java 物件與 JSON 的轉換

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;

@Path("/user")
public class UserResource {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public String getUser() {
        User user = new User();
        user.name = "John";
        user.age = 30;

        Jsonb jsonb = JsonbBuilder.create();
        return jsonb.toJson(user);  // 將物件序列化為 JSON
    }
}

需要將 JSON 轉換為 Java 物件(反序列化),可以這樣做

String json = "{\"name\":\"John\",\"age\":30}";
User user = jsonb.fromJson(json, User.class);  // 將 JSON 反序列化為物件

使用 Jackson

Quarkus 也支援 Jackson

步驟 1:新增相依套件

./mvnw quarkus:add-extension -Dextensions="jackson"

步驟 2:使用 Jackson 註解

在POJO 類別加上 Jackson 的註解

import com.fasterxml.jackson.annotation.JsonProperty;

public class User {
    @JsonProperty("name")
    public String name;

    @JsonProperty("age")
    public int age;

    // 建構子、getter 和 setter
}

步驟 3:使用 Jackson 進行序列化與反序列化

使用 Jackson 的 ObjectMapper 來進行轉換

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import com.fasterxml.jackson.databind.ObjectMapper;

@Path("/user")
public class UserResource {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public String getUser() throws Exception {
        User user = new User();
        user.name = "John";
        user.age = 30;

        ObjectMapper mapper = new ObjectMapper();
        return mapper.writeValueAsString(user);  // 將物件序列化為 JSON
    }
}

將 JSON 反序列化為 Java 物件,可以這樣做:

String json = "{\"name\":\"John\",\"age\":30}";
ObjectMapper mapper = new ObjectMapper();
User user = mapper.readValue(json, User.class);  // 將 JSON 反序列化為物件

使用 MapStruct

輕鬆自動將一個物件的屬性值映射到另一個物件上

步驟 1:新增相依套件

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>1.5.5.Final</version> <!-- 可根據需要選擇最新版本 -->
    </dependency>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-processor</artifactId>
        <version>1.5.5.Final</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

步驟 2:定義映射介面

定義一個介面,使用 MapStruct 的 @Mapper 註解,並描述要進行映射的來源和目標物件:

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface UserMapper {
    UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);

    // 定義物件轉換方法
    UserDTO toUserDTO(User user);

    User toUser(UserDTO userDTO);
}

步驟 3:建立 POJO 類別

定義你需要映射的資料物件:

public class User {
    public String name;
    public int age;

    // 建構子、getter 和 setter
}

public class UserDTO {
    public String name;
    public int age;

    // 建構子、getter 和 setter
}

步驟 4:使用映射器

在你的業務邏輯中使用 MapStruct 自動生成的映射器進行物件轉換:

// User 物件轉換成 UserDTO
User user = new User();
user.setName("John");
user.setAge(30);

UserDTO userDTO = UserMapper.INSTANCE.toUserDTO(user);

上一篇
開發-Quarkus急迫上手-Part1
下一篇
開發-Quarkus急迫上手-Part3 : 附雲端簡談收尾
系列文
微服務奇兵:30天Quarkus特訓營30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言