本篇咱們從零開發一個quarkus應用,支持虛擬線程響應web服務,響應式操作postgresql資料庫,並且在quarkus官方還未支持的情況下,率先並將其製作成docker鏡像 ...
歡迎訪問我的GitHub
這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos
本篇概覽
- 本篇是《支持JDK19虛擬線程的web框架》系列的中篇,前文咱們體驗了有虛擬線程支持的web服務,經過測試,發現性能上它與其他兩種常見web架構並無明顯區別,既然如此,還有必要研究和學習嗎?
- 當然有必要,而且還要通過實戰更深入瞭解虛擬線程與常規線程的區別,在各大框架和庫廣泛支持虛擬線程之前,打好理論和實踐基礎,這才是本系列的目標
- 為了接下來的深入瞭解,咱們先在本篇打好基礎:詳細說明前文的web功能是如何開發出來的
- 為了突出重點,這裡先提前劇透,從編碼的角度說清楚如何開啟虛擬線程支持,其實非常簡單,如下圖,左側是quarkus框架下的一個普通web服務,每收到一個web請求,是由線程池中的線程負責響應的,右側的web服務多了個@RunOnVirtualThread註解,就變成了由新建的虛擬線程去處理web請求,沒錯,在quarkus框架下使用虛擬線程就是這麼簡單
- 在前文中,我們通過返回值也看到了上述兩個web服務中,負責web響應的線程的不同,如下所示,從線程名稱上很容易看出線程池和虛擬線程的區別
-
看到這裡,您可能會說:就這?一個註解就搞定的事情,你還要寫一篇文章?這不是在浪費作者你自己和各位讀者的時間嗎?
-
確實,開啟虛擬線程,編碼只要一行,然而就目前而言,虛擬線程是JDK19專屬,而且還只是預覽功能,要想在實際運行的時候真正開啟並不容易,需要從JDK、maven、IDE等方方面面都要做相關設置,而且如果要做成前文那樣的docker鏡像,一行docker run命令就能開啟虛擬線程,還要在Dockerfile上做點事情(quarkus提供的基礎鏡像中沒有JDK19版本,另外啟動命令也要調整)
-
上述這些都是本文的重點,欣宸已經將這些梳理清楚了,接下來咱們一起實戰吧,讓前文體驗過的web從無到有,再到順利運行,達到預期
-
整個開發過程如下圖所示,一共十步,接下來開始動手
開發環境
- 開發電腦:MacBook Pro M1,macOS Monterey 12.6
- IDE:IntelliJ IDEA 2022.3 EAP (Ultimate Edition) (即未發佈前的早期預覽版)
- 另外,M1晶元的電腦上開發和運行JDK19應用,與普通的X86相比感受不到任何變化,只有一點要註意:上傳docker鏡像到hub.docker.com時,鏡像的系統架構是ARM的,這樣的鏡像在X86電腦上下載下來後不能運行
下載JDK19
- 下載jdk19,由於電腦是M1晶元,我選擇的jdk是azul版本,地址是:https://www.azul.com/downloads/?package=jdk#download-openjdk
- 使用azul的jdk和之前的oracle版本並無區別,至少在開發環境感受不到,來看下azul官方的說法
- 實際上,azul的jdk很全面,x86晶元的各平臺版本安裝包都提供了,您可以根據自己電腦環境選擇下載,下麵是我選擇的適合M1晶元的版本
- 下載完成後雙擊安裝即可
修改maven的配置
- 我這裡使用的是本地maven,其對應的JDK也要改成19,修改方法是調整環境變數JAVA_HOME,令其指向JDK19目錄(在我的電腦上,環境變數是在~/.zshrc裡面)
- 修改後令環境變數生效,然後執行一下命令確認已經使用了JDK19
➜ ~ mvn -version
Apache Maven 3.8.5 (3599d3414f046de2324203b78ddcf9b5e4388aa0)
Maven home: /Users/zhaoqin/software/apache-maven-3.8.5
Java version: 19, vendor: Azul Systems, Inc., runtime: /Library/Java/JavaVirtualMachines/zulu-19.jdk/Contents/Home
Default locale: zh_CN_#Hans, platform encoding: UTF-8
OS name: "mac os x", version: "12.6", arch: "aarch64", family: "mac"
創建Quarkus項目
- 打開IDEA,新建項目,選擇Quarkus項目
- 接下來選擇要用到的擴展包(其實就是在圖形化頁面添加jar依賴),這裡的選擇如下圖:Reactive PostgreSQL client和RESTEasy Reactive Jackson
- 點擊上圖右下角的Create按鈕後項目開始創建,稍作等待,項目創建完成,如下圖,此刻只能感慨:quarkus太貼心,不但有demo源碼,還有各種版本的Dockerfile文件,而且git相關的配置也有,甚至README.md都寫得那麼詳細,我是不是可以點擊運行按鈕直接把程式run起來了
IDEA設置
- 由於要用到JDK19,下麵幾項設置需要檢查並確認
- 首先是Project設置,如下圖
- 其次是Modules設置,先配置Sources這個tab頁
- 接下來是Dependencies這個tab頁
- 進入IDEA系統設置菜單
- 如下圖,三個位置需要設置
- 設置完成了,接下來開始編碼
編碼
- 首先確認pom.xml,這是IDEA幫我們創建的,內容如下,有兩處改動稍後會說到
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bolingcavalry</groupId>
<artifactId>quarkus-virual-threads-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<compiler-plugin.version>3.8.1</compiler-plugin.version>
<maven.compiler.release>19</maven.compiler.release>
<maven.compiler.source>19</maven.compiler.source>
<maven.compiler.target>19</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>2.13.2.Final</quarkus.platform.version>
<skipITs>true</skipITs>
<surefire-plugin.version>3.0.0-M7</surefire-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>${quarkus.platform.artifact-id}</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-reactive-pg-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
<!-- 生成測試數據 -->
<dependency>
<groupId>net.datafaker</groupId>
<artifactId>datafaker</artifactId>
<version>1.6.0</version>
</dependency>
<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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
</goals>
</execution>
</executions>
<!-- 這裡是新增的虛擬線程相關特性,start -->
<configuration>
<source>19</source>
<target>19</target>
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
<jvmArgs>--enable-preview --add-opens java.base/java.lang=ALL-UNNAMED</jvmArgs>
</configuration>
<!-- 這裡是新增的虛擬線程相關特性,end -->
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner
</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<properties>
<skipITs>false</skipITs>
<quarkus.package.type>native</quarkus.package.type>
</properties>
</profile>
</profiles>
</project>
- pom.xml的第一處改動如下圖,要確保全部是19
- 第二處改動,是在quarkus-maven-plugin插件中增加額外的配置參數,如下圖紅框
- 接下來新增配置文件application.properties,在resources目錄下
quarkus.datasource.db-kind=postgresql
quarkus.datasource.jdbc.max-size=8
quarkus.datasource.jdbc.min-size=2
quarkus.datasource.username=quarkus
quarkus.datasource.password=123456
quarkus.datasource.reactive.url=postgresql://192.168.0.1:5432/quarkus_test
- 開始寫java代碼了,首先是啟動類VirtualThreadsDemoApp.java
package com.bolingcavalry;
import io.quarkus.runtime.Quarkus;
import io.quarkus.runtime.annotations.QuarkusMain;
@QuarkusMain
public class VirtualThreadsDemoApp {
public static void main(String... args) {
Quarkus.run(args);
}
}
- 資料庫對應的model類有兩個,第一個是gender欄位的枚舉
package com.bolingcavalry.model;
public enum Gender {
MALE, FEMALE;
}
- 表對應的實體類
package com.bolingcavalry.model;
import io.vertx.mutiny.sqlclient.Row;
public class Person {
private Long id;
private String name;
private int age;
private Gender gender;
private Integer externalId;
public String getThreadInfo() {
return threadInfo;
}
public void setThreadInfo(String threadInfo) {
this.threadInfo = threadInfo;
}
private String threadInfo;
public Person() {
}
public Person(Long id, String name, int age, Gender gender, Integer externalId) {
this.id = id;
this.name = name;
this.age = age;
this.gender = gender;
this.externalId = externalId;
this.threadInfo = Thread.currentThread().toString();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Gender getGender() {
return gender;
}
public void setGender(Gender gender) {
this.gender = gender;
}
public Integer getExternalId() {
return externalId;
}
public void setExternalId(Integer externalId) {
this.externalId = externalId;
}
public static Person from(Row row) {
return new Person(
row.getLong("id"),
row.getString("name"),
row.getInteger("age"),
Gender.valueOf(row.getString("gender")),
row.getInteger("external_id"));
}
}
- 接下來是操作資料庫的dao類,可見使用操作方式還是很原始的,還要在代碼中手寫SQL,取出也要逐個欄位匹配,其實quarkus也支持JPA,只不過本篇使用的是響應式資料庫驅動,所以選用的是Vert.x生成的連接池PgPool
package com.bolingcavalry.repository;
import com.bolingcavalry.model.Person;
import io.vertx.mutiny.pgclient.PgPool;
import io.vertx.mutiny.sqlclient.Row;
import io.vertx.mutiny.sqlclient.RowSet;
import io.vertx.mutiny.sqlclient.Tuple;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;
@ApplicationScoped
public class PersonRepositoryAsyncAwait {
@Inject
PgPool pgPool;
public Person findById(Long id) {
RowSet<Row> rowSet = pgPool
.preparedQuery("SELECT id, name, age, gender, external_id FROM person WHERE id = $1")
.executeAndAwait(Tuple.of(id));
List<Person> persons = iterateAndCreate(rowSet);
return persons.size() == 0 ? null : persons.get(0);
}
private List<Person> iterateAndCreate(RowSet<Row> rowSet) {
List<Person> persons = new ArrayList<>();
for (Row row : rowSet) {
persons.add(Person.from(row));
}
return persons;
}
}
- 接下來就是前面截圖看到的web服務類VTPersonResource.java,它被註解@RunOnVirtualThread修飾,表示收到web請求在虛擬線程中執行響應代碼
package com.bolingcavalry.resource;
import com.bolingcavalry.model.Person;
import com.bolingcavalry.repository.PersonRepositoryAsyncAwait;
import io.smallrye.common.annotation.RunOnVirtualThread;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@Path("/vt/persons")
@RunOnVirtualThread
public class VTPersonResource {
@Inject
PersonRepositoryAsyncAwait personRepository;
@GET
@Path("/{id}")
public Person getPersonById(@PathParam("id") Long id) {
return personRepository.findById(id);
}
}
- 最後是用於對比的常規web服務類PoolPersonResource.java,這個就是中規中矩的線上程池中取一個線程來執行響應代碼
package com.bolingcavalry.resource;
import com.bolingcavalry.model.Person;
import com.bolingcavalry.repository.PersonRepositoryAsyncAwait;
import io.smallrye.common.annotation.RunOnVirtualThread;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@Path("/pool/persons")
public class PoolPersonResource {
@Inject
PersonRepositoryAsyncAwait personRepository;
@GET
@Path("/{id}")
public Person getPersonById(@PathParam("id") Long id) {
return personRepository.findById(id);
}
}
- 至此,編碼完成
IDEA啟動設置
- 編碼完成後,在IDEA上啟動應用做本地調試是咱們的基本操作,所以IDEA運行環境也要設置成支持JDK19的預覽特性
- 打開入口類,點擊main方法前面的綠色箭頭,在彈出的菜單上選擇Modify Run Configuration
- 在運行應用的設置頁面,如下操作
- 選中Add VM options
- 填入下圖箭頭所指的內容
- 終於,設置完成,接下來要啟動應用了
啟動和驗證
- 啟動應用之前,請確認postgresql資料庫已啟動,並且數據已經導入,具體啟動和導入方法請參考前文
- 點擊下圖紅色箭頭中指向的按鈕,即可在IDEA中運行應用
- 瀏覽器訪問地址:http://localhost:8080/vt/persons/1 ,如下圖,符合預期
- 在前文中,咱們是在docker上運行應用的,另外在實際場景中應用運行在docker或者k8s環境也是普遍情況,所以接下來一起實戰將用做成docker鏡像並驗證
構建鏡像
- 在創建工程的時候,IDEA就用quarkus模板自動創建了多個Dockerfile文件,下圖紅框中全是
- 如果當前應用的JDK不是19,而是11或者17,那麼上圖紅框中的Dockerfile文件就能直接使用了,然而,由於今天咱們應用的JDK必須是19,就無法使用這些Dockerfile了,必須自己寫一個,原因很簡單,打開Dockerfile.jvm,如下圖紅色箭頭所示,基礎鏡像是jdk17,而這個倉庫中並沒有JDK19,也就是說quarkus還沒有發佈JDK19版本的基礎鏡像,咱們要自己找一個,另外,容器啟動命令也要調整,需要加入--enable-preview才能開啟JVM的虛擬線程
- 自己寫的Dockerile文件名為Dockerfile.19,內容如下,可見非常簡單:先換基礎鏡像,再把mvn構建結果複製過去,最後加個啟動命令就完事兒了(遠不如官方的分層構建節省空間,然而在官方的JDK19鏡像方案出來之前,先用下麵這個將就著用吧)
FROM openjdk:19
ENV LANGUAGE='en_US:en'
# 執行工作目錄
WORKDIR application
COPY --chown=185 target/*.jar ./
RUN mkdir config
EXPOSE 8080
USER 185
ENTRYPOINT ["java", "-jar", "--enable-preview", "quarkus-virual-threads-demo-1.0-SNAPSHOT-runner.jar"]
-
接下來可以製作鏡像了,請確保自己電腦上docker已在運行
-
首先是常規maven編譯打包(uber-jar表示生成的jar中包含了所有依賴庫)
mvn clean package -U -DskipTests -Dquarkus.package.type=uber-jar
- 構建docker鏡像
docker build -f src/main/docker/Dockerfile.19 -t bolingcavalry/quarkus-virual-threads-demo:0.0.2 .
- 鏡像製作成功,控制台輸出如下圖
- 如果您有hub.docker.com的賬號,也可以像我一樣推送到公共倉庫,方便大家使用
異常測試(沒有enable-preview參數會怎麼樣?)
- 回顧Dockerfile中啟動應用的命令,由於虛擬線程是JDK19的預覽功能,因此必須添加下圖紅色箭頭所指的--enable-preview參數才能讓虛擬線程功能生效
- 於是我就在想:不加這個參數會咋樣?也就是不開啟虛擬線程,但是代碼中卻要用它,那麼真正運行的時候會如何呢?
- 瞎猜是沒用的,還是試試吧,在啟動參數中刪除--enable-preview,如下圖,再重新構建鏡像
- 像前文那樣運行容器(再次提醒,確保資料庫是正常的),再在瀏覽器訪問http://localhost:8080/vt/persons/1,頁面正常顯示了,看來功能是不受影響的
- 再用docker logs命令查看後臺日誌,如下圖箭頭所示,quarkus給出了WARN級別的提示:由於當前虛擬機不支持虛擬線程,改為使用預設的阻塞來執行業務邏輯
- 小結:在不支持虛擬線程的環境強行使用虛擬線程,quarkus會選擇相容的方式繼續完成任務
小結和展望
-
至此,一個完整的quarkus應用已開發完成,該應用使用虛擬線程來響應web請求,而且在quarkus官方還沒有提供方案的前提下,咱們依舊完成了docker鏡像的製作,最後,因為好奇,還關閉重要參數嘗試了一下,一系列操作下來,相信您已經對基礎開發瞭如指掌了
-
最後,還剩下兩個遺留問題,相信您也會有類似困惑
- 虛擬線程和常規子線程的區別,究竟能不能看出來?前文已經驗證了性能上區別不大,那還有別的方式來觀察和區分嗎?
- 能不能稍微深入一點,僅憑一個@RunOnVirtualThread註解就強行寫了兩篇博客,實在是太忽悠人了
-
以上問題會在接下來的《支持JDK19虛擬線程的web框架,終篇》得到解決,還是那句熟悉的廣告詞:欣宸原創,不辜負您的期待