使用Quarkus在Openshift上構建微服務的快速指南


在我的博客上,您有機會閱讀了許多關於使用Spring Boot或Micronaut之類框架構建微服務的文章。這里將介紹另一個非常有趣的框架專門用於微服務體系結構,它越來越受到大家的關注– Quarkus。它是作為下一代Kubernetes/Openshift原生Java框架引入的。它構建在著名的Java標准之上,如CDI、JAX-RS和Eclipse MicroProfile,這些標准將它與Spring Boot區別開來。

其他一些可能說服您使用Quarkus的特性包括非常快的啟動時間、為在容器中運行而優化的最小內存占用,以及較短的首次請求時間。此外,盡管它是一個相對較新的框架(當前版本是0.21),但它有很多擴展,包括Hibernate、Kafka、RabbitMQ、Openapi和Vert.x等等。

在本文中,我將指導您使用Quarkus構建微服務,並在OpenShift(通過Minishift)上運行它們。我們將討論以下主題:

  • 構建基於rest的且包含輸入校驗的應用程序
  • 微服務與RestClient之間的通信
  • 開放健康檢查(liveness, readiness)
  • 開放OpenAPI /Swagger 文檔
  • 使用Quarkus Maven插件在本地機器上運行應用程序
  • 使用JUnit和RestAssured進行測試
  • 使用source-2鏡像在Minishift上部署和運行Quarkus應用程序

1. 創建應用程序 - 依賴項

在創建新應用程序時,你可以執行一個Maven命令,該命令使用quarkus-maven-plugin。依賴項應該在參數-Dextensions中聲明。

mvn io.quarkus:quarkus-maven-plugin:0.21.1:create \
    -DprojectGroupId=pl.piomin.services \
    -DprojectArtifactId=employee-service \
    -DclassName="pl.piomin.services.employee.controller.EmployeeController" \
    -Dpath="/employees" \
    -Dextensions="resteasy-jackson, hibernate-validator"

下面是我們pom.xml的結構:

<properties>
    <quarkus.version>0.21.1</quarkus.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-bom</artifactId>
            <version>${quarkus.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<build>
    <plugins>
        <plugin>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-maven-plugin</artifactId>
            <version>${quarkus.version}</version>
            <executions>
                <execution>
                    <goals>
                        <goal>build</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

對於使用輸入驗證構建簡單的REST應用程序,我們不需要太多模塊。您可能已經注意到,我只聲明了兩個擴展,這與下面pom.xml中的依賴項列表相同:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-validator</artifactId>
</dependency>

2. 創建應用程序 - 代碼

對於Spring Boot或Micronaut用戶來說,可能有點奇怪的是,沒有使用靜態代碼main方法的主運行類。resource/controller類實際上就是主類。Quarkus的resource/controller類和方法應該使用javax.ws.rs庫中的注解進行標記。

下面是employee-service的REST controller 的實現:

@Path("/employees")
@Produces(MediaType.APPLICATION_JSON)
public class EmployeeController {

    private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeController.class);

    @Inject
    EmployeeRepository repository;

    @POST
    public Employee add(@Valid Employee employee) {
        LOGGER.info("Employee add: {}", employee);
        return repository.add(employee);
    }

    @Path("/{id}")
    @GET
    public Employee findById(@PathParam("id") Long id) {
        LOGGER.info("Employee find: id={}", id);
        return repository.findById(id);
    }

    @GET
    public Set<Employee> findAll() {
        LOGGER.info("Employee find");
        return repository.findAll();
    }

    @Path("/department/{departmentId}")
    @GET
    public Set<Employee> findByDepartment(@PathParam("departmentId") Long departmentId) {
        LOGGER.info("Employee find: departmentId={}", departmentId);
        return repository.findByDepartment(departmentId);
    }

    @Path("/organization/{organizationId}")
    @GET
    public Set<Employee> findByOrganization(@PathParam("organizationId") Long organizationId) {
        LOGGER.info("Employee find: organizationId={}", organizationId);
        return repository.findByOrganization(organizationId);
    }

}

我們使用CDI進行依賴注入,使用SLF4J進行日志記錄。 Controller類使用內存存儲庫bean存儲和檢索數據。Repository bean使用CDI @ApplicationScoped注解,並注入controller:

@ApplicationScoped
public class EmployeeRepository {

    private Set<Employee> employees = new HashSet<>();

    public EmployeeRepository() {
        add(new Employee(1L, 1L, "John Smith", 30, "Developer"));
        add(new Employee(1L, 1L, "Paul Walker", 40, "Architect"));
    }

    public Employee add(Employee employee) {
        employee.setId((long) (employees.size()+1));
        employees.add(employee);
        return employee;
    }

    public Employee findById(Long id) {
        Optional<Employee> employee = employees.stream().filter(a -> a.getId().equals(id)).findFirst();
        if (employee.isPresent())
            return employee.get();
        else
            return null;
    }

    public Set<Employee> findAll() {
        return employees;
    }

    public Set<Employee> findByDepartment(Long departmentId) {
        return employees.stream().filter(a -> a.getDepartmentId().equals(departmentId)).collect(Collectors.toSet());
    }

    public Set<Employee> findByOrganization(Long organizationId) {
        return employees.stream().filter(a -> a.getOrganizationId().equals(organizationId)).collect(Collectors.toSet());
    }

}

最后一個組件是帶驗證的實體類:

public class Employee {

    private Long id;
    @NotNull
    private Long organizationId;
    @NotNull
    private Long departmentId;
    @NotBlank
    private String name;
    @Min(1)
    @Max(100)
    private int age;
    @NotBlank
    private String position;

    // ... GETTERS AND SETTERS

}

3. 單元測試

對於大多數流行的Java框架,使用Quarkus進行單元測試非常簡單。如果您正在測試基於REST的web應用程序,您應該在pom.xml中包含以下依賴項:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <scope>test</scope>
</dependency>

讓我們分析一下來自organization-service(我們的另一個微服務,以及employee-service和department-service)的測試類。測試類應該用@QuarkusTest注釋。我們可以通過@Inject注解注入其他bean。其余部分是典型的JUnit和RestAssured—我們正在測試controller公開的API方法。因為我們使用內存存儲庫,所以除了服務間通信之外,我們不需要模擬任何東西(我們將在本文后面討論)。對於GET、POST方法,我們有一些積極的場景,還有一個不通過輸入驗證的消極場景(testInvalidAdd)。

@QuarkusTest
public class OrganizationControllerTests {

    @Inject
    OrganizationRepository repository;

    @Test
    public void testFindAll() {
        given().when().get("/organizations").then().statusCode(200).body(notNullValue());
    }

    @Test
    public void testFindById() {
        Organization organization = new Organization("Test3", "Address3");
        organization = repository.add(organization);
        given().when().get("/organizations/{id}", organization.getId()).then().statusCode(200)
                .body("id", equalTo(organization.getId().intValue()))
                .body("name", equalTo(organization.getName()));
    }

    @Test
    public void testFindByIdWithDepartments() {
        given().when().get("/organizations/{id}/with-departments", 1L).then().statusCode(200)
                .body(notNullValue())
                .body("departments.size()", is(1));
    }

    @Test
    public void testAdd() {
        Organization organization = new Organization("Test5", "Address5");
        given().contentType("application/json").body(organization)
                .when().post("/organizations").then().statusCode(200)
                .body("id", notNullValue())
                .body("name", equalTo(organization.getName()));
    }

    @Test
    public void testInvalidAdd() {
        Organization organization = new Organization();
        given().contentType("application/json").body(organization).when().post("/organizations").then().statusCode(400);
    }

}

4. 服務間通信

由於Quarkus的目標是在Kubernetes上運行,因此它不提供任何對第三方服務發現(例如通過Consul 或Netflix Eureka)和與此發現集成的HTTP客戶機的內置支持。然而,Quarkus為REST通信提供了專用的客戶端支持。要使用它,我們首先需要包括以下依賴性:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-client</artifactId>
</dependency>

Quarkus基於MicroProfile REST客戶機提供聲明性REST客戶機。您需要創建一個帶有所需方法的接口,並使用@RegisterRestClient對其進行注解。其他注解與服務器端非常相似。因為您使用@RegisterRestClient來標記Quarkus,所以應該知道這個接口作為REST客戶機可用於CDI注入。

@Path("/departments")
@RegisterRestClient
public interface DepartmentClient {

    @GET
    @Path("/organization/{organizationId}")
    @Produces(MediaType.APPLICATION_JSON)
    List<Department> findByOrganization(@PathParam("organizationId") Long organizationId);

    @GET
    @Path("/organization/{organizationId}/with-employees")
    @Produces(MediaType.APPLICATION_JSON)
    List<Department> findByOrganizationWithEmployees(@PathParam("organizationId") Long organizationId);

}

現在,讓我們看一下organization-service內的controller類。與@Inject一起,我們需要使用@RestClient注解來正確地注入REST客戶機bean。之后,您可以使用接口方法來調用其他公開的服務

@Path("/organizations")
@Produces(MediaType.APPLICATION_JSON)
public class OrganizationController {

    private static final Logger LOGGER = LoggerFactory.getLogger(OrganizationController.class);

    @Inject
    OrganizationRepository repository;
    @Inject
    @RestClient
    DepartmentClient departmentClient;
    @Inject
    @RestClient
    EmployeeClient employeeClient;

    // ... OTHER FIND METHODS

    @Path("/{id}/with-departments")
    @GET
    public Organization findByIdWithDepartments(@PathParam("id") Long id) {
        LOGGER.info("Organization find: id={}", id);
        Organization organization = repository.findById(id);
        organization.setDepartments(departmentClient.findByOrganization(organization.getId()));
        return organization;
    }

    @Path("/{id}/with-departments-and-employees")
    @GET
    public Organization findByIdWithDepartmentsAndEmployees(@PathParam("id") Long id) {
        LOGGER.info("Organization find: id={}", id);
        Organization organization = repository.findById(id);
        organization.setDepartments(departmentClient.findByOrganizationWithEmployees(organization.getId()));
        return organization;
    }

    @Path("/{id}/with-employees")
    @GET
    public Organization findByIdWithEmployees(@PathParam("id") Long id) {
        LOGGER.info("Organization find: id={}", id);
        Organization organization = repository.findById(id);
        organization.setEmployees(employeeClient.findByOrganization(organization.getId()));
        return organization;
    }

}

通信中缺少的最后一個東西是目標服務的地址。我們可以使用@RegisterRestClient注解的字段baseUri 來提供它們。然而,更好的解決方案似乎是將它們放在application.properties中。屬性名需要包含客戶端接口的完全限定名和后綴mp-rest/url

pl.piomin.services.organization.client.DepartmentClient/mp-rest/url=http://localhost:8090
pl.piomin.services.organization.client.EmployeeClient/mp-rest/url=http://localhost:8080

在前一節中,我已經提到了單元測試和服務間通信。要測試與其他應用程序通信的API方法,我們需要模擬REST客戶機。下面是為模擬示例創建了DepartmentClient。它應該只在測試期間可見,所以我們必須將它放在src/test/java中。如果我們用@Mock@RestClient注釋它,那么默認情況下將自動使用這個bean,而不是在src/main/java中定義的聲明性REST客戶機。

@Mock
@ApplicationScoped
@RestClient
public class MockDepartmentClient implements DepartmentClient {

    @Override
    public List<Department> findByOrganization(Long organizationId) {
        return Collections.singletonList(new Department("Test1"));
    }

    @Override
    public List<Department> findByOrganizationWithEmployees(Long organizationId) {
        return null;
    }

}

5. 監測和記錄

我們可以輕松地使用Quarkus公開健康檢查或API文檔。API文檔是使用OpenAPI/Swagger構建的。Quarkus利用了 SmallRye項目中可用的庫。我們應該在pom.xml中包含以下依賴項:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-health</artifactId>
</dependency>

我們可以定義兩種類型的健康檢查:readiness 和liveness。有/health/ready/health/live上下文路徑。要將它們公開到應用程序之外,我們需要定義一個實現MicroProfile HealthCheck 接口的bean。Readiness 端應該用@Readiness標注,而liveness 端應該用@Liveness標注。

@ApplicationScoped
@Readiness
public class ReadinessHealthcheck implements HealthCheck {

    @Override
    public HealthCheckResponse call() {
        return HealthCheckResponse.named("Employee Health Check").up().build();
    }

}

為了啟用Swagger文檔,我們只需要添加一個依賴項即可。Quarkus還為Swagger提供了內置UI。默認情況下,它是在開發模式下啟用的,所以如果您願意在生產環境中使用它,您應該添加quarkus.swagger-ui.always-include=true到您的application.properties文件。現在,如果通過執行Maven命令mvn compile quarkus:dev在本地以開發模式運行應用程序employee-service,您可以在URLhttp://localhost:8080/swagger-ui下查看可用的API規范。

quarkus-swagger

這是我從應用程序啟動時的日志。它打印監聽端口和加載的擴展列表。

quarkus-startup

6. 在本地機器上運行微服務

因為我們希望在同一台機器上運行多個應用程序,所以需要覆蓋它們的默認HTTP監聽端口。雖然employee-service仍然在默認的8080 端口上運行,但是其他微服務使用不同的端口,如下所示。

department-service:
quarkus-port-department

organization-service:
quarkus-port-organization

讓我們測試一下Swagger UI中的服務間通信。我調用了GET /organizations/{id}/with-departments,它調用由department-service公開的端點GET GET /departments/organization/{organizationId}。結果如下圖所示。

quarkus-communication

7. 在OpenShift上運行微服務

我們已經完成了示例微服務體系結構的實現,並在本地機器上運行它們。現在,我們可以進行最后一步,並嘗試在 Minishift上部署這些應用程序。在OpenShift上部署Quarkus應用程序時,我們有一些不同的方法。今天,我將向您展示如何利用S2I為此構建的機制。

我們將使用Quarkus GraalVM Native S2I Builder。可以在 quai.io的 quarkus/ubi-quarkus-native-s2i找到。當然,在部署應用程序之前,我們需要先啟動Minishift。根據Quarkus的文檔,基於GraalVM的本機構建占用了大量內存和CPU,所以我決定為Minishift設置6GB和4個內核。

$ minishift start --vm-driver=virtualbox --memor

此外,我們還需要稍微修改一下應用程序的源代碼。您可能還記得,我們使用JDK 11在本地運行它們。Quarkus S2I builder只支持JDK 8,所以我們需要在pom.xml中更改它。我們還需要包括一個聲明的本機配置文件如下:

<properties>
    <quarkus.version>0.21.1</quarkus.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>
...
<profiles>
    <profile>
        <id>native</id>
        <activation>
            <property>
                <name>native</name>
            </property>
        </activation>
        <build>
            <plugins>
                <plugin>
                    <groupId>io.quarkus</groupId>
                    <artifactId>quarkus-maven-plugin</artifactId>
                    <version>${quarkus.version}</version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>native-image</goal>
                            </goals>
                            <configuration>
                                <enableHttpUrlHandler>true</enableHttpUrlHandler>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <artifactId>maven-failsafe-plugin</artifactId>
                    <version>2.22.1</version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>integration-test</goal>
                                <goal>verify</goal>
                            </goals>
                            <configuration>
                                <systemProperties>
                                    <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
                                </systemProperties>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

另外在application.properties文件需要修改兩處。我們不需要覆蓋端口號,因為 Minishift動態地為每個pod分配虛擬IP。服務間的通信是通過OpenShift發現實現的,所以我們只需要設置服務的名稱而不是localhost。

quarkus.swagger-ui.always-include=true
pl.piomin.services.organization.client.DepartmentClient/mp-rest/url=http://department:8080
pl.piomin.services.organization.client.EmployeeClient/mp-rest/url=http://employee:8080

最后,我們可以將我們的應用程序部署到Minishift上。為此,你應使用oc客戶端執行以下命令:

$ oc new-app quay.io/quarkus/ubi-quarkus-native-s2i:19.1.1~https://github.com/piomin/sample-quarkus-microservices.git#openshift --context-dir=employee --name=employee
$ oc new-app quay.io/quarkus/ubi-quarkus-native-s2i:19.1.1~https://github.com/piomin/sample-quarkus-microservices.git#openshift --context-dir=department --name=department
$ oc new-app quay.io/quarkus/ubi-quarkus-native-s2i:19.1.1~https://github.com/piomin/sample-quarkus-microservices.git#openshift --context-dir=organization --name=organization

正如您所看到的,可以在我的GitHub帳戶上找到找到程序源代碼,地址是https://github.com/piomin/sample-quarkus-microservices.git。在Minishift 上運行的版本已經在分支openshift中共享。在本地機器上運行的版本在主分支上可用。因為所有的應用程序都存儲在一個庫中,所以我們需要為每個部署定義一個參數context-dir

我很失望。雖然為minishift 設置更多的內存和CPU花費了我很長的時間——大約25分鍾。

quarkus-builds

然而,經過長時間的等待,我的所有應用程序終於都部署好了。

quarkus-openshift-overview

我通過執行下面可見的命令將它們公開在Minishift 外。可以使用DNS http://${APP_NAME}-myproject.192.168.99.100.nip.io下的OpenShift路由測試它們。

$ oc expose svc employee
$ oc expose svc department
$ oc expose svc organization

此外,您還可以在OpenShift上啟用readiness 和liveness 健康檢查,因為它們在默認情況下是禁用的。

quarkus-health

9月福利,關注公眾號

后台回復:004,領取8月翻譯集錦!

往期福利回復:001,002, 003即可領取!

img


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM