在個人博客上,您有機會閱讀了許多關於使用Spring Boot或Micronaut之類框架構建微服務的文章。這裏將介紹另外一個很是有趣的框架專門用於微服務體系結構,它愈來愈受到你們的關注– Quarkus。它是做爲下一代Kubernetes/Openshift原生Java框架引入的。它構建在著名的Java標準之上,如CDI、JAX-RS和Eclipse MicroProfile,這些標準將它與Spring Boot區別開來。java
其餘一些可能說服您使用Quarkus的特性包括很是快的啓動時間、爲在容器中運行而優化的最小內存佔用,以及較短的首次請求時間。此外,儘管它是一個相對較新的框架(當前版本是0.21),但它有不少擴展,包括Hibernate、Kafka、RabbitMQ、Openapi和Vert.x等等。git
在本文中,我將指導您使用Quarkus構建微服務,並在OpenShift(經過Minishift)上運行它們。咱們將討論如下主題:github
在建立新應用程序時,你能夠執行一個Maven命令,該命令使用quarkus-maven-plugin
。依賴項應該在參數-Dextensions
中聲明。web
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
的結構:json
<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
中的依賴項列表相同:api
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>複製代碼
對於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:app
@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
}複製代碼
對於大多數流行的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);
}
}複製代碼
因爲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;
}
}複製代碼
咱們能夠輕鬆地使用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規範。
這是我從應用程序啓動時的日誌。它打印監聽端口和加載的擴展列表。
由於咱們但願在同一臺機器上運行多個應用程序,因此須要覆蓋它們的默認HTTP監聽端口。雖然employee-service仍然在默認的8080
端口上運行,可是其餘微服務使用不一樣的端口,以下所示。
department-service:
organization-service:
讓咱們測試一下Swagger UI中的服務間通訊。我調用了GET /organizations/{id}/with-departments
,它調用由department-service公開的端點GET GET /departments/organization/{organizationId}
。結果以下圖所示。
咱們已經完成了示例微服務體系結構的實現,並在本地機器上運行它們。如今,咱們能夠進行最後一步,並嘗試在 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分鐘。
然而,通過長時間的等待,個人全部應用程序終於都部署好了。
我經過執行下面可見的命令將它們公開在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 健康檢查,由於它們在默認狀況下是禁用的。
9月福利,關注公衆號後臺回覆:004,領取8月翻譯集錦!往期福利回覆:001,002, 003便可領取!