• 支持JDK19虚拟线程的web框架,之二:完整开发一个支持虚拟线程的quarkus应用


    欢迎访问我的GitHub

    这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

    本篇概览

    • 本篇是《支持JDK19虚拟线程的web框架》系列的中篇,前文咱们体验了有虚拟线程支持的web服务,经过测试,发现性能上它与其他两种常见web架构并无明显区别,既然如此,还有必要研究和学习吗?
    • 当然有必要,而且还要通过实战更深入了解虚拟线程与常规线程的区别,在各大框架和库广泛支持虚拟线程之前,打好理论和实践基础,这才是本系列的目标
    • 为了接下来的深入了解,咱们先在本篇打好基础:详细说明前文的web功能是如何开发出来的
    • 为了突出重点,这里先提前剧透,从编码的角度说清楚如何开启虚拟线程支持,其实非常简单,如下图,左侧是quarkus框架下的一个普通web服务,每收到一个web请求,是由线程池中的线程负责响应的,右侧的web服务多了个@RunOnVirtualThread注解,就变成了由新建的虚拟线程去处理web请求,没错,在quarkus框架下使用虚拟线程就是这么简单

    image-20221019081651928

    • 在前文中,我们通过返回值也看到了上述两个web服务中,负责web响应的线程的不同,如下所示,从线程名称上很容易看出线程池和虚拟线程的区别

    image-20221019083324043

    • 看到这里,您可能会说:就这?一个注解就搞定的事情,你还要写一篇文章?这不是在浪费作者你自己和各位读者的时间吗?

    • 确实,开启虚拟线程,编码只要一行,然而就目前而言,虚拟线程是JDK19专属,而且还只是预览功能,要想在实际运行的时候真正开启并不容易,需要从JDK、maven、IDE等方方面面都要做相关设置,而且如果要做成前文那样的docker镜像,一行docker run命令就能开启虚拟线程,还要在Dockerfile上做点事情(quarkus提供的基础镜像中没有JDK19版本,另外启动命令也要调整)

    • 上述这些都是本文的重点,欣宸已经将这些梳理清楚了,接下来咱们一起实战吧,让前文体验过的web从无到有,再到顺利运行,达到预期

    • 整个开发过程如下图所示,一共十步,接下来开始动手

    image-20221020081857985

    开发环境

    • 开发电脑: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

    image-20221015082121173
    • 实际上,azul的jdk很全面,x86芯片的各平台版本安装包都提供了,您可以根据自己电脑环境选择下载,下面是我选择的适合M1芯片的版本
    image-20221015082450750
    • 下载完成后双击安装即可

    修改maven的配置

    • 我这里使用的是本地maven,其对应的JDK也要改成19,修改方法是调整环境变量JAVA_HOME,令其指向JDK19目录(在我的电脑上,环境变量是在~/.zshrc里面)

    image-20221015091138678

    • 修改后令环境变量生效,然后执行一下命令确认已经使用了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项目
    image-20221015083615179
    • 接下来选择要用到的扩展包(其实就是在图形化页面添加jar依赖),这里的选择如下图:Reactive PostgreSQL clientRESTEasy Reactive Jackson
    image-20221015084459075
    • 点击上图右下角的Create按钮后项目开始创建,稍作等待,项目创建完成,如下图,此刻只能感慨:quarkus太贴心,不但有demo源码,还有各种版本的Dockerfile文件,而且git相关的配置也有,甚至README.md都写得那么详细,我是不是可以点击运行按钮直接把程序run起来了
    image-20221015085440544

    IDEA设置

    • 由于要用到JDK19,下面几项设置需要检查并确认
    • 首先是Project设置,如下图

    image-20221015112614328

    • 其次是Modules设置,先配置Sources这个tab页

    image-20221015112738859

    • 接下来是Dependencies这个tab页
    image-20221015112819900
    • 进入IDEA系统设置菜单
    image-20221015112115388
    • 如下图,三个位置需要设置
    image-20221015113055594
    • 设置完成了,接下来开始编码

    编码

    • 首先确认pom.xml,这是IDEA帮我们创建的,内容如下,有两处改动稍后会说到
    
    <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.0modelVersion>
        <groupId>com.bolingcavalrygroupId>
        <artifactId>quarkus-virual-threads-demoartifactId>
        <version>1.0-SNAPSHOTversion>
        <properties>
            <compiler-plugin.version>3.8.1compiler-plugin.version>
            <maven.compiler.release>19maven.compiler.release>
            <maven.compiler.source>19maven.compiler.source>
            <maven.compiler.target>19maven.compiler.target>
            <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
            <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
            <quarkus.platform.artifact-id>quarkus-bomquarkus.platform.artifact-id>
            <quarkus.platform.group-id>io.quarkus.platformquarkus.platform.group-id>
            <quarkus.platform.version>2.13.2.Finalquarkus.platform.version>
            <skipITs>trueskipITs>
            <surefire-plugin.version>3.0.0-M7surefire-plugin.version>
        properties>
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>${quarkus.platform.group-id}groupId>
                    <artifactId>${quarkus.platform.artifact-id}artifactId>
                    <version>${quarkus.platform.version}version>
                    <type>pomtype>
                    <scope>importscope>
                dependency>
            dependencies>
        dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.quarkusgroupId>
                <artifactId>quarkus-resteasy-reactive-jacksonartifactId>
            dependency>
            <dependency>
                <groupId>io.quarkusgroupId>
                <artifactId>quarkus-reactive-pg-clientartifactId>
            dependency>
            <dependency>
                <groupId>io.quarkusgroupId>
                <artifactId>quarkus-arcartifactId>
            dependency>
            <dependency>
                <groupId>io.quarkusgroupId>
                <artifactId>quarkus-resteasy-reactiveartifactId>
            dependency>
    
            
            <dependency>
                <groupId>net.datafakergroupId>
                <artifactId>datafakerartifactId>
                <version>1.6.0version>
            dependency>
    
            <dependency>
                <groupId>io.quarkusgroupId>
                <artifactId>quarkus-junit5artifactId>
                <scope>testscope>
            dependency>
            <dependency>
                <groupId>io.rest-assuredgroupId>
                <artifactId>rest-assuredartifactId>
                <scope>testscope>
            dependency>
        dependencies>
        <build>
            <plugins>
                <plugin>
                    <groupId>${quarkus.platform.group-id}groupId>
                    <artifactId>quarkus-maven-pluginartifactId>
                    <version>${quarkus.platform.version}version>
                    <extensions>trueextensions>
                    <executions>
                        <execution>
                            <goals>
                                <goal>buildgoal>
                                <goal>generate-codegoal>
                                <goal>generate-code-testsgoal>
                            goals>
                        execution>
                    executions>
                    
                    <configuration>
                        <source>19source>
                        <target>19target>
                        <compilerArgs>
                            <arg>--enable-previewarg>
                        compilerArgs>
                        <jvmArgs>--enable-preview --add-opens java.base/java.lang=ALL-UNNAMEDjvmArgs>
                    configuration>
                    
                plugin>
                <plugin>
                    <artifactId>maven-compiler-pluginartifactId>
                    <version>${compiler-plugin.version}version>
                    <configuration>
                        <compilerArgs>
                            <arg>-parametersarg>
                        compilerArgs>
                    configuration>
                plugin>
                <plugin>
                    <artifactId>maven-surefire-pluginartifactId>
                    <version>${surefire-plugin.version}version>
                    <configuration>
                        <systemPropertyVariables>
                            <java.util.logging.manager>org.jboss.logmanager.LogManagerjava.util.logging.manager>
                            <maven.home>${maven.home}maven.home>
                        systemPropertyVariables>
                    configuration>
                plugin>
                <plugin>
                    <artifactId>maven-failsafe-pluginartifactId>
                    <version>${surefire-plugin.version}version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>integration-testgoal>
                                <goal>verifygoal>
                            goals>
                            <configuration>
                                <systemPropertyVariables>
                                    <native.image.path>${project.build.directory}/${project.build.finalName}-runner
                                    native.image.path>
                                    <java.util.logging.manager>org.jboss.logmanager.LogManagerjava.util.logging.manager>
                                    <maven.home>${maven.home}maven.home>
                                systemPropertyVariables>
                            configuration>
                        execution>
                    executions>
                plugin>
            plugins>
        build>
        <profiles>
            <profile>
                <id>nativeid>
                <activation>
                    <property>
                        <name>nativename>
                    property>
                activation>
                <properties>
                    <skipITs>falseskipITs>
                    <quarkus.package.type>nativequarkus.package.type>
                properties>
            profile>
        profiles>
    project>
    
    
    • pom.xml的第一处改动如下图,要确保全部是19
    image-20221020082700997
    • 第二处改动,是在quarkus-maven-plugin插件中增加额外的配置参数,如下图红框
    image-20221020082849667
    • 接下来新增配置文件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 rowSet = pgPool
               .preparedQuery("SELECT id, name, age, gender, external_id FROM person WHERE id = $1")
               .executeAndAwait(Tuple.of(id));
            List persons = iterateAndCreate(rowSet);
            return persons.size() == 0 ? null : persons.get(0);
        }
    
        private List iterateAndCreate(RowSet rowSet) {
            List 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
    image-20221015115712663
    • 在运行应用的设置页面,如下操作

    image-20221015115542673

    • 选中Add VM options
    image-20221015115859652
    • 填入下图箭头所指的内容
    image-20221015120024775
    • 终于,设置完成,接下来要启动应用了

    启动和验证

    • 启动应用之前,请确认postgresql数据库已启动,并且数据已经导入,具体启动和导入方法请参考前文
    • 点击下图红色箭头中指向的按钮,即可在IDEA中运行应用
    image-20221021081112061 image-20221021082603972
    • 在前文中,咱们是在docker上运行应用的,另外在实际场景中应用运行在docker或者k8s环境也是普遍情况,所以接下来一起实战将用做成docker镜像并验证

    构建镜像

    • 在创建工程的时候,IDEA就用quarkus模板自动创建了多个Dockerfile文件,下图红框中全是
    image-20221021083348600
    • 如果当前应用的JDK不是19,而是11或者17,那么上图红框中的Dockerfile文件就能直接使用了,然而,由于今天咱们应用的JDK必须是19,就无法使用这些Dockerfile了,必须自己写一个,原因很简单,打开Dockerfile.jvm,如下图红色箭头所示,基础镜像是jdk17,而这个仓库中并没有JDK19,也就是说quarkus还没有发布JDK19版本的基础镜像,咱们要自己找一个,另外,容器启动命令也要调整,需要加入--enable-preview才能开启JVM的虚拟线程
    image-20221021083635218
    • 自己写的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 .
    
    • 镜像制作成功,控制台输出如下图
    image-20221022072718957
    • 如果您有hub.docker.com的账号,也可以像我一样推送到公共仓库,方便大家使用

    异常测试(没有enable-preview参数会怎么样?)

    • 回顾Dockerfile中启动应用的命令,由于虚拟线程是JDK19的预览功能,因此必须添加下图红色箭头所指的--enable-preview参数才能让虚拟线程功能生效
    image-20221022073455393
    • 于是我就在想:不加这个参数会咋样?也就是不开启虚拟线程,但是代码中却要用它,那么真正运行的时候会如何呢?
    • 瞎猜是没用的,还是试试吧,在启动参数中删除--enable-preview,如下图,再重新构建镜像
    image-20221022074847459
    • 像前文那样运行容器(再次提醒,确保数据库是正常的),再在浏览器访问http://localhost:8080/vt/persons/1,页面正常显示了,看来功能是不受影响的
    image-20221022082633379
    • 再用docker logs命令查看后台日志,如下图箭头所示,quarkus给出了WARN级别的提示:由于当前虚拟机不支持虚拟线程,改为使用默认的阻塞来执行业务逻辑
    image-20221022075007690
    • 小结:在不支持虚拟线程的环境强行使用虚拟线程,quarkus会选择兼容的方式继续完成任务

    小结和展望

    • 至此,一个完整的quarkus应用已开发完成,该应用使用虚拟线程来响应web请求,而且在quarkus官方还没有提供方案的前提下,咱们依旧完成了docker镜像的制作,最后,因为好奇,还关闭重要参数尝试了一下,一系列操作下来,相信您已经对基础开发了如指掌了

    • 最后,还剩下两个遗留问题,相信您也会有类似困惑

      1. 虚拟线程和常规子线程的区别,究竟能不能看出来?前文已经验证了性能上区别不大,那还有别的方式来观察和区分吗?
      2. 能不能稍微深入一点,仅凭一个@RunOnVirtualThread注解就强行写了两篇博客,实在是太忽悠人了
    • 以上问题会在接下来的《支持JDK19虚拟线程的web框架,终篇》得到解决,还是那句熟悉的广告词:欣宸原创,不辜负您的期待

    欢迎关注博客园:程序员欣宸

    学习路上,你不孤单,欣宸原创一路相伴...

  • 相关阅读:
    驱动点灯实验
    java计算机毕业设计在线学习跟踪系统后台源码+数据库+系统+lw文档+mybatis+运行部署
    【Flink实战】新老用户分析:按照操作系统维度进行新老用户的分析
    【Element-UI】CUD(增删改)及form 表单验证(附源码)
    故障分析 | MySQL锁等待超时一例分析
    真正“搞”懂HTTP协议14之HTTP3
    Git实用小技巧
    1360. 日期之间隔几天
    Hyper-V Ubuntu 虚拟机配置双网卡
    农产品商城毕业设计,农产品销售系统毕业设计,农产品电商毕业设计论文方案需求分析作品参考
  • 原文地址:https://www.cnblogs.com/bolingcavalry/p/17689826.html