Quarkus云原生框架入门体验

背景描述

Java并不是为了Web而诞生,但似乎B/S架构让Java生机无限,Spring全家桶的助推也使得Java在Web更为强大,微服务体系Spring Cloud更是顺风顺水,不得不说的Spring应用的痛点就是启动过慢,内存占用偏高,对服务器资源占用较大,而且JVM的本身就难逃离内存的过度依赖。

随着容器化技术Docker、Kubernetes,让云原生似乎成为了未来的发展方向,云原生(Cloud-Native)这个概念最早由Pivotal公司的Matt Stine于2013年首次提出,提到云原生首先想到的关键词可能就是容器化、微服务、Lambda,服务网格等,当然这些是必要元素,但是不代表拥有这些元素就是云原生应用,很多应用的部署只能说是基于云来完成,比如私有云、公有云,这也是未来的趋势。云原生本质上不是部署,而是以什么方式来构建应用,云原生的最终目的是为了提高开发效率,提升业务敏捷度、扩容性、可用性、资源利用率,降低成本。

Go语言作为一种云原生语言也体现出了强大的生命力,Java也在变化,2018年Java出现了大量轻量级微服务框架,来面对未来的云原生趋势,Red Hat推出的Quarkus、Oracle的Helidon以及Spring Native都在快速发展,拥抱云原生。

什么是Quarkus

传统的Java技术栈是为了单机应用设计的,需要耗费大量的内存和CPU,并且启动速度慢,当然也不存在云、容器、k8s等技术体系,在云原生时代,Java技术需要一个变革去迎接这个新世界的挑战。
Quarkus就是为了应对云原生而设计,并且是针对HotSpot、GraalVM定制化的k8s native框架,目标是使 Java 成为 Kubernetes 和serverless环境中的领先平台,同时为开发人员提供一个框架来解决更广泛的分布式应用程序架构。这也反应在官方对Quarkus的介绍Supersonic Subatomic Java。

Quarkus有哪些特殊的地方,官方列举了以下特点。
image.png
这些特点里面最过于吸引人的应该就是:

Quarkus提供了k8s扩展支持,可以让开发人员不用关心k8s底层复杂的框架设计,也可以很好的使用其进行应用部署。此外还提供了native镜像的能力,大大提高了应用的启动时间和减小了内存的占用,同时对命令式和反应式编程的支持,也加大了容错性,反应式可以提高应用对大量请求的响应速度。
quarkus_metrics_graphic_bootmem_wide.png

官方文档:CREATING YOUR FIRST APPLICATION

准备

使用Quarkus需要准备环境

  • GraalVM Open-JDK-11
  • Maven 3.8.1+
  • Quarkus 2.6.2.Final

更多内容查看文档:https://quarkus.io/guides/getting-started

下载

Quarkus官方网址:https://quarkus.io
GraalVM下载地址:

下载时候注意Graal有基于不同JDK版本的支持,如JDK8,JDK11,JDK11,可根据自己需求下载。

环境变量

配置环境变量方式同Java一样

  • 配置JAVA_HOME或者GRAALVM_HOME C:\Program Files\Java\graalvm-ce-java8-21.0.0.2
  • 环境变量配置不生效解决方案:

多个JDK版本情况下出现环境变量切换不生效,Windows本身系统system32里面的环境变量加载等级要优先于用户设置的环境变量,其根本原因是%JAVA_HOME%在path中配置的位置在%SystemRoot%\system32;后面,放到path最前面就好了。
image.png

  • 配置好后执行命令确保使用的是GraalVM
C:\Users\starsray>java -version
openjdk version "1.8.0_282"
OpenJDK Runtime Environment (build 1.8.0_282-b07)
OpenJDK 64-Bit Server VM GraalVM CE 21.0.0.2 (build 25.282-b07-jvmci-21.0-b06, mixed mode)

创建Web应用

使用maven创建

使用maven来快速创建一个应用,创建的位置在当前命令执行终端所在的根目录,项目名称为getting-started。

  • Linux & MacOS
mvn io.quarkus.platform:quarkus-maven-plugin:2.6.2.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=getting-started \
    -DclassName="org.acme.getting.started.GreetingResource" \
    -Dpath="/hello"
  • Windows
    • cmd
    mvn io.quarkus.platform:quarkus-maven-plugin:2.6.2.Final:create -DprojectGroupId=org.acme -DprojectArtifactId=getting-started -DclassName="org.acme.getting.started.GreetingResource" -Dpath="/hello"
    
    • powershell
    mvn io.quarkus.platform:quarkus-maven-plugin:2.6.2.Final:create "-DprojectGroupId=org.acme" "-DprojectArtifactId=getting-started" "-DclassName=org.acme.getting.started.GreetingResource" "-Dpath=/hello"
    

使用web页面创建

使用web页面:访问通过网站:https://code.quarkus.io,这种方法类似于spring.io提供的web页面

使用IDEA创建

使用IDEA,这里不得不说IDEA的强大,其实也是去加载https://code.quarkus.ioweb资源。

image.png

运行应用

GraalVM运行

项目创建后通过IDEA打开加载相关依赖启动项目,在项目根目录下,执行命令启动

./mvnw compile quarkus:dev:

或者

mvn quarkus:dev

成功启动应用后,查看控制台输出

Listening for transport dt_socket at address: 5005
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2022-01-20 14:38:01,336 INFO  [io.quarkus] (Quarkus Main Thread) getting-started 1.0.0-SNAPSHOT on JVM (powered by Quarkus 2.6.2.Final) started in 3.635s. 
Listening on: http://localhost:8080

2022-01-20 14:38:01,347 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2022-01-20 14:38:01,348 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, resteasy, smallrye-context-propagation, vertx]

通过地址访问 http://localhost:8080,Quarkus提供了一个Dev UI。
image.png

如果配置文件引入了数据库相关驱动,却没有指定可用的jdbc配置信息,启动项目会要求有可用的docker环境,自动创建docker镜像并运行,这也说明了quarkus对云原生的良好支持。

/opt/develop/graalvm-ce-java11-21.3.0/bin/java -Dmaven.multiModuleProjectDirectory=/home/starsray -Dmaven.home=/opt/ideaIU-2021.2.3/plugins/maven/lib/maven3 -Dclassworlds.conf=/opt/ideaIU-2021.2.3/plugins/maven/lib/maven3/bin/m2.conf -javaagent:/opt/ideaIU-2021.2.3/lib/idea_rt.jar=43095:/opt/ideaIU-2021.2.3/bin -Dfile.encoding=UTF-8 -classpath /opt/ideaIU-2021.2.3/plugins/maven/lib/maven3/boot/plexus-classworlds.license:/opt/ideaIU-2021.2.3/plugins/maven/lib/maven3/boot/plexus-classworlds-2.6.0.jar org.codehaus.classworlds.Launcher -Didea.version=2021.2.3 -DskipTests=true quarkus:dev
[INFO] Scanning for projects...
[INFO] 
[INFO] ----------------------< cn.starsray:quarkus-web >-----------------------
[INFO] Building quarkus-web 1.0.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- quarkus-maven-plugin:2.6.2.Final:dev (default-cli) @ quarkus-web ---
[INFO] Invoking org.apache.maven.plugins:maven-resources-plugin:2.6:resources) @ quarkus-web
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource
[INFO] Invoking io.quarkus.platform:quarkus-maven-plugin:2.6.2.Final:generate-code) @ quarkus-web
[INFO] Invoking org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile) @ quarkus-web
[INFO] Nothing to compile - all classes are up to date
[INFO] Invoking org.apache.maven.plugins:maven-resources-plugin:2.6:testResources) @ quarkus-web
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /home/starsray/IdeaProjects/quarkus-web/quarkus-web/src/test/resources
[INFO] Invoking io.quarkus.platform:quarkus-maven-plugin:2.6.2.Final:generate-code-tests) @ quarkus-web
[INFO] Invoking org.apache.maven.plugins:maven-compiler-plugin:3.8.1:testCompile) @ quarkus-web
[INFO] Nothing to compile - all classes are up to date
Listening for transport dt_socket at address: 5005
Press [h] for more options>
Tests paused
Press [r] to resume testing, [h] for more options>
Press [r] to resume testing, [o] Toggle test output, [h] for more options>
2022-01-16 23:25:11,416 INFO  [org.tes.doc.DockerClientProviderStrategy] (build-37) Loaded org.testcontainers.dockerclient.UnixSocketClientProviderStrategy from ~/.testcontainers.properties, will try it first
2022-01-16 23:25:11,888 INFO  [org.tes.doc.DockerClientProviderStrategy] (build-37) Found Docker environment with local Unix socket (unix:///var/run/docker.sock)
2022-01-16 23:25:11,890 INFO  [org.tes.DockerClientFactory] (build-37) Docker host IP address is localhost
2022-01-16 23:25:11,935 INFO  [org.tes.DockerClientFactory] (build-37) Connected to docker: 
  Server Version: 19.03.8
  API Version: 1.40
  Operating System: UnionTech OS Desktop 20 Home
  Total Memory: 15688 MB
2022-01-16 23:25:11,938 INFO  [org.tes.uti.ImageNameSubstitutor] (build-37) Image name substitution will be performed by: DefaultImageNameSubstitutor (composite of 'ConfigurationFileImageNameSubstitutor' and 'PrefixingImageNameSubstitutor')
2022-01-16 23:25:12,005 INFO  [org.tes.uti.RegistryAuthLocator] (build-37) Failure when attempting to lookup auth config. Please ignore if you don't have images in an authenticated registry. Details: (dockerImageName: testcontainers/ryuk:0.3.3, configFile: /home/starsray/.docker/config.json. Falling back to docker-java default behaviour. Exception message: /home/starsray/.docker/config.json (没有那个文件或目录)
2022-01-16 23:25:12,676 INFO  [org.tes.DockerClientFactory] (build-37) Ryuk started - will monitor and terminate Testcontainers containers on JVM exit
2022-01-16 23:25:12,676 INFO  [org.tes.DockerClientFactory] (build-37) Checking the system...
2022-01-16 23:25:12,677 INFO  [org.tes.DockerClientFactory] (build-37) ✔︎ Docker server version should be at least 1.6.0
2022-01-16 23:25:12,755 INFO  [org.tes.DockerClientFactory] (build-37) ✔︎ Docker environment should have more than 2GB free disk space
2022-01-16 23:25:12,780 INFO  [🐳 .io/.0.24]] (build-37) Creating container for image: docker.io/mysql:8.0.24
2022-01-16 23:25:12,781 INFO  [org.tes.uti.RegistryAuthLocator] (build-37) Failure when attempting to lookup auth config. Please ignore if you don't have images in an authenticated registry. Details: (dockerImageName: docker.io/mysql:8.0.24, configFile: /home/starsray/.docker/config.json. Falling back to docker-java default behaviour. Exception message: /home/starsray/.docker/config.json (没有那个文件或目录)
2022-01-16 23:25:12,874 INFO  [🐳 .io/.0.24]] (build-37) Starting container with ID: ec24cbadb7cdd72ce95cbdcb01011173939705e4c7685c070074fa7c0fbb270e
2022-01-16 23:25:13,122 INFO  [🐳 .io/.0.24]] (build-37) Container docker.io/mysql:8.0.24 is starting: ec24cbadb7cdd72ce95cbdcb01011173939705e4c7685c070074fa7c0fbb270e
2022-01-16 23:25:13,131 INFO  [🐳 .io/.0.24]] (build-37) Waiting for database connection to become available at jdbc:mysql://localhost:32773/default using query 'SELECT 1'
2022-01-16 23:25:26,498 INFO  [🐳 .io/.0.24]] (build-37) Container is started (JDBC URL: jdbc:mysql://localhost:32773/default)
2022-01-16 23:25:26,499 INFO  [🐳 .io/.0.24]] (build-37) Container docker.io/mysql:8.0.24 started in PT13.732728S
2022-01-16 23:25:26,499 INFO  [io.qua.dev.mys.dep.MySQLDevServicesProcessor] (build-37) Dev Services for MySQL started.
2022-01-16 23:25:26,500 INFO  [io.qua.dat.dep.dev.DevServicesDatasourceProcessor] (build-37) Dev Services for the default datasource (mysql) started.
__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/   
2022-01-16 23:25:27,968 ERROR [io.qua.hib.orm.run.sch.SchemaManagementIntegrator] (Hibernate post-boot validation thread for <default>) Failed to validate Schema: Schema-validation: missing table [employee]
2022-01-16 23:25:27,979 ERROR [io.qua.hib.orm.run.sch.SchemaManagementIntegrator] (Hibernate post-boot validation thread for <default>) The following SQL may resolve the database issues, as generated by the Hibernate schema migration tool. WARNING: You must manually verify this SQL is correct, this is a best effort guess, do not copy/paste it without verifying that it does what you expect.

create table employee (emp_no integer not null, birth_date date not null, first_name varchar(14) not null, gender char(1) not null, hire_date date not null, last_name varchar(16) not null, primary key (emp_no)) engine=InnoDB;

2022-01-16 23:25:28,003 INFO  [io.quarkus] (Quarkus Main Thread) quarkus-web 1.0.0-SNAPSHOT on JVM (powered by Quarkus 2.6.2.Final) started in 17.707s. Listening on: http://localhost:8080
2022-01-16 23:25:28,006 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2022-01-16 23:25:28,006 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [agroal, cdi, config-yaml, hibernate-orm, hibernate-orm-panache, jdbc-mysql, mybatis, mybatis-plus, narayana-jta, reactive-mysql-client, rest-client, rest-client-jackson, resteasy, resteasy-jackson, smallrye-context-propagation, smallrye-openapi, swagger-ui, vertx]

可以看到在启动过程中创建了Docker容器,并且互通两个容器。

Native Image运行

这也是quarkus最强大的地方,通过GraalVM的可选组件native image可以将Java应用打包成原生镜像,真正做到秒级启动,解决了传统spring启动的痛点,当然也期待spring的下一代产品Spring Native

  • 使用命令安装native支持
sudo gu install native-image
  • 打包项目
mvn package -Pnative -DskipTests
  • 启动 做到了秒启动。

image.png

注意事项

启动编译失败

如果本地存在多个JDK版本,项目启动编译失败,请确保Java和Maven的版本是否正确,正确配置后重启IDEA。

  • java
$ java -version
openjdk version "11.0.13" 2021-10-19
OpenJDK Runtime Environment GraalVM CE 21.3.0 (build 11.0.13+7-jvmci-21.3-b05)
OpenJDK 64-Bit Server VM GraalVM CE 21.3.0 (build 11.0.13+7-jvmci-21.3-b05, mixed mode, sharing)
  • maven
$ mvn -v
Apache Maven 3.8.4 (9b656c72d54e5bacbed989b64718c159fe39b537)
Maven home: D:\maven\apache-maven-3.8.4
Java version: 11.0.13, vendor: GraalVM Community, runtime: C:\Program Files\Java\graalvm-ce-java11-21.3.0
Default locale: zh_CN, platform encoding: GBK
OS name: "windows 10", version: "10.0", arch: "amd64", family: "windows"

windows native build

native image需要先打包native可执行文件,我这里使用的是Linux环境,所以打包没什么问题,在Windows环境下打包始终不能成功,参考官方文档也一直未能成功打包,可能跟我使用的Windows11环境有关,有兴趣的小伙伴可以自己试试。

项目介绍

对比Spring应用,Quarkus应用在目录结构上并没有太大不同,主要是一些依赖实现的区别。Spring针对JavaEE规范有一套自己的实现,Quarkus整合了多个优秀的开源框架实现,类似于Spring Cloud对微服务组件的整合。Quarkus主要整合了以下常用组件,有兴趣的可以单独了解

在getting-started项目中主要了解以下三方面内容

  • pom文件
  • REST Resource
  • Bean

Quarkus项目目录树结构

├─.idea
├─.mvn
│  └─wrapper
├─src
│  ├─main
│  │  ├─docker
│  │  ├─java
│  │  │  └─org
│  │  │      └─acme
│  │  │          └─getting
│  │  │              └─started
│  │  └─resources
│  │      └─META-INF
│  │          └─resources
│  └─test
│      └─java
│          └─org
│              └─acme
│                  └─getting
│                      └─started
└─target
    └─classes
        └─META-INF
            └─resources

pom.xml

在pom.xml中默认依赖了Quarkus的BOM清单,在中做了关于Quarkus的统一版本管理,类似于spring-boot-dependencies的功能。

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>${quarkus.platform.group-id}</groupId>
            <artifactId>quarkus-bom</artifactId>
            <version>${quarkus.platform.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

pom.xml中还添加了打包相关的插件,使用quarkus-maven-plugin来进行打包、启动、native image等操作。

<build>
    <plugins>
        <plugin>
            <groupId>${quarkus.platform.group-id}</groupId>
            <artifactId>quarkus-maven-plugin</artifactId>
            <version>${quarkus-plugin.version}</version>
            <extensions>true</extensions>
            <executions>
                <execution>
                    <goals>
                        <goal>build</goal>
                        <goal>generate-code</goal>
                        <goal>generate-code-tests</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

其中quarkus-resteasy是REST应用的依赖

<dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-resteasy</artifactId>
</dependency>

其他依赖

  • quarkus-smallrye-openapi openapi实现引入支持swagger-ui
  • quarkus-mybatis/quarkus-mybatis-plus mybatis及mybatis-plus组件
  • quarkus-hibernate-orm-panache hibernate JPA实现
  • quarkus-config-yaml yaml语法配置文件相关依赖
  • quarkus-reactive-mysql-client mysql客户端依赖
  • quarkus-jdbc-mysql mysql JDBC驱动依赖
  • quarkus-resteasy-jackson rest接口依赖

REST Resource

项目中默认生成了GreetingResource一个REST资源类

package org.acme.getting.started;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

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

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

@Path、@GET、@Produces都是基于JAX-RS规范实现的注解。在Spring盛行的今天,JavaWeb方向国内几乎人人都是Spring程序员,对于JavaEE的规范了解使用都不多。
JAX-RS是JAVA EE6引入的一个新技术,英文全称为Java API for RESTful Web Services,它的核心概念是面向资源,即Resource形式,常见的实现有Jersey、RESTEasy,就好比JPA的实现Hibernate。
关于JAX-RS参考:https://cloud.tencent.com/developer/article/1600752

Bean

Spring核心的功能就是IoC和DI,DI是指依赖注入,Quarkus中依赖注入使用了ArC,ArC是JavaEE规范中的CDI的一个实现,是Quarkus定制化的依赖注入组件。
CDI全称Contexts and Dependency Injection,即上下文依赖注入,JEE众多规范中的一个,从JavaEE开始CDI成为正式规范。CDI参考文档:

Quarkus CDI参考文档:https://quarkus.io/guides/cdi
通过CDI来初始化一个被容器管理的Bean时候都会指定Bean的生命周期,在Spring中指定的Bean默认都是单例的,生命周期通过@Scope注解来修改。
CDI提供了提供了一系列注解,根据注解名称就能看出修饰Bean生命周期。

  • @ApplicationScoped
  • @SessionScoped
  • @RequestScoped
  • @ConversationScoped

在Quarkus中不需要指定创建Application级别的类,一般也不需要特定标注Bean的生命周期。

With Quarkus, there is no need to create an Application class. It’s supported, but not required. In addition, only one instance of the resource is created and not one per request. You can configure this using the different *Scoped annotations (ApplicationScoped, RequestScoped, etc)

Quarkus同样支持类似于Spring中Controller注入Service的形式,新增一个GreetingService类

package org.acme.getting.started;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class GreetingService {

    public String greeting(String name) {
        return "hello " + name;
    }

}

修改GreetResource类,并注入GreetingService类

package org.acme.getting.started;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.jboss.resteasy.annotations.jaxrs.PathParam;

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

    @Inject
    GreetingService service;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/greeting/{name}")
    public String greeting(@PathParam String name) {
        return service.greeting(name);
    }

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

开发

Development Mode

通过quarkus:dev命令会使应用在开发模式启动,可以对pom.xml、Java类、Resouce目录下的变动进行热加载,减少重启,这大大加快了开发调试效率。
debug模式会默认监听5005端口,如果想要在项目启动前进入调试模式通过命令行指定 -Dsuspend,也可以通过-Ddebug=false跳过所有调试。

Testing

在生成的pom.xml文件中默认引入了test依赖和相关打包插件

<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>

<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>

创建的项目中默认包含了一个测试类GreetingResourceTest

package org.acme.getting.started;

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;

import java.util.UUID;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;

@QuarkusTest
public class GreetingResourceTest {

    @Test    
    public void testHelloEndpoint() {
        given()
          .when().get("/hello")
          .then()
             .statusCode(200)    
             .body(is("hello"));
    }

    @Test
    public void testGreetingEndpoint() {
        String uuid = UUID.randomUUID().toString();
        given()
          .pathParam("name", uuid)
          .when().get("/hello/greeting/{name}")
          .then()
            .statusCode(200)
            .body(is("hello " + uuid));
    }

}

类似于SpringBootRunner,Quarkus使用QuarkusTest runner来运行,校验返回结果。

Package

项目打包可以使以下命令,打包后的可执行jar或者native-image在./target目录。

  • ./mvnw package & mvn package
  • mvn package -Pnative -DskipTests(打包native)

其他

Quarkus也提供了类似于SpringBoot banner的功能。

  • 禁用banner
    • quarkus.banner.enabled=false (application.properties)
    • -Dquarkus.banner.enabled=false(Java System Property)
    • QUARKUS_BANNER_ENABLED=false (ENV)
  • 自定义banner,添加文件到src/main/resources目录,在application.properties中指定文件路径
    • quarkus.banner.path=name-of-file

Spring对比

注解相关说明:
Spring中有一套自己针对相关功能的注解实现,Quarkus也有自己的整合实现,列举相关的对应关系。
DI --> CDI
Spring Web --> JAX-RS
Spring Data JPA --> Panache
Swagger/Knife --> OpenAPI

创建Reactive Web应用

Reactive(反应式)与Imperative(命令式)相对应, Reactive一般都关联着back-pressure, monads, or event-driven architecture等词汇,也往往表现出一些特点。

  • Responsive - they must respond in a timely fashion(及时响应)
  • Elastic - they adapt themselves to the fluctuating load(弹性伸缩)
  • Resilient - they handle failures gracefully(优雅的处理失败)
  • Asynchronous message passing - the component of a reactive system interact using messages(消息交互)

更多内容:https://principles.reactive.foundation

Reactive 反应式并不是Java所特有,是一种应对高并发、快速响应编程的一系列原则,Spring 5提供了全新的Spring WebFlux框架,相比于传统的Spring MVC,实现了Reactive Streams规范,不需要Servlet API的支持,创建基于事件循环执行模型的完全异步且非阻塞的应用程序。
Quarkus同样提供了对Reactive编程的支持。这些原则在使用资源(CPU和内存)的同时更有效地处理比传统方法更多的负载,同时也会优雅地反应失败。Quarkus相比于Spring WebFlux的优势在于你不用纠结如何选择,可以在同一个应用程序中同时使用反应式和非反应式编程,不需要依赖第三方应用或者技术栈。

Quarkus可以作为反应式和非反应式应用的桥梁。

Quarkus如何实现反应式,借用官方描述的一张图片,通过Eclipse Vert.x and Netty等构建了一个特有的引擎,来处理non-blocking I/O交互。Quarkus或者应用可以通过代码来编排数据库、消息队列等I/O事件交互。
image

创建应用

同创建Quarkus Web应用类似,创建反应式应用只需要选择相应的反应式组件即可。

  • Hibernate Reactive with Panache
  • RESTEasy Reactive
  • Reactive MySQL client

pom.xml对应的依赖

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-hibernate-reactive-panache</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-reactive-mysql-client</artifactId>
</dependency>

创建对应的实体类

package com.starsray.entity;

import io.quarkus.hibernate.reactive.panache.PanacheEntity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Lob;
import javax.persistence.Table;
import java.time.LocalDate;

@Entity
@Table(name = "employee")
public class Employee extends PanacheEntity {
    @Column(name = "emp_no", nullable = false)
    private Integer empNo;

    @Column(name = "birth_date", nullable = false)
    private LocalDate birthDate;

    @Column(name = "first_name", nullable = false, length = 14)
    private String firstName;

    @Column(name = "last_name", nullable = false, length = 16)
    private String lastName;

    @Lob
    @Column(name = "gender", nullable = false)
    private String gender;

    @Column(name = "hire_date", nullable = false)
    private LocalDate hireDate;

    public LocalDate getHireDate() {
        return hireDate;
    }

    public void setHireDate(LocalDate hireDate) {
        this.hireDate = hireDate;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public LocalDate getBirthDate() {
        return birthDate;
    }

    public void setBirthDate(LocalDate birthDate) {
        this.birthDate = birthDate;
    }

    public Integer getEmpNo() {
        return empNo;
    }

    public void setEmpNo(Integer empNo) {
        this.empNo = empNo;
    }
}

创建实体类对应的Resource,里面包含一个Reactive API和普通API。

package com.starsray;

import com.starsray.entity.Employee;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.smallrye.mutiny.Uni;

import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.List;

@ApplicationScoped
@Path("employee")
public class EmployeeReactiveResource {


    @GET
    @Path("/{empNo}")
    @Produces
    public Uni<List<Employee>> get(@PathParam("empNo")int empNo) {
        return Employee.list("id", 1L);
    }

    @POST
    @Path("create")
    @Produces
    @Consumes
    public Uni<Response> create(Employee Employee) {
        return Panache.<Employee>withTransaction(Employee::persist)
                .onItem()
                .transform(inserted -> Response.created(URI.create("/Employees/" + inserted.id))
                        .build());
    }


    @GET
    @Path("/{id}")
    @Produces
    public Uni<Employee> getSingle(@PathParam("id") Long id) {
        return Employee.findById(id);
    }
}

配置数据库连接

quarkus.datasource.db-kind=mysql
quarkus.datasource.username=root
quarkus.datasource.password=root
quarkus.datasource.reactive.url=mysql://localhost:3306/employees
quarkus.hibernate-orm.log.format-sql=true
quarkus.hibernate-orm.log.sql=true

启动项目

./mvnw quarkus:dev

在体验上几乎看不出来区别。Quarkus底层都已经实现了。

Reactive

由于Reactive通过一种异步非阻塞式I/O和数据库进行交互,因此需要一种异步HTTP实现结构体,Quarkus使用Mutiny 作为其核心的反应式编程模块,因此HTTP请求支持返回两种Mutiny类型(Uni and Multi),在引入的Hibernate Reactive with Panache模块中只需要实体类继承PanacheEntity类,就可以暴漏并使用这两种返回类型。没有特殊指定。RESTEasy Reactive会默认返回List对象为JSONArray。

  • GET

注意查看这一段代码,返回类型是通过Uni来包装的实体Value,没有直接返回结果集。当数据库读取到相关数据后,Uni会获取并返回结果。

@GET
@Path("/{empNo}")
@Produces
public Uni<List<Employee>> get(@PathParam("empNo") int empNo) {
    return Employee.list("id", 1L);
}

Uni<?>是一种异步返回类型,有点类似future,Uni作为一个占位符等待结果(暂称为Item)返回,当接收到Mutiny分发的Item时,开发人员可以进行一些业务逻辑的处理,也可以表达为Reactive的延续性(相对于传统命令式阻塞I/O的顺序性),体现在获取一个Uni,当Uni占位符得到分发的Item时,执行其他过程。

为什么返回类型是通过Uni来返回Item,通过关系型数据库查询,直接返回List结果集并不是一种好的方式,传统关系型数据库并不能很好的处理流式结果,也并没有相关设计协议。通过流来返回数据库结果,需要保持连接或者开启一个事务直到所有的查询行被消费,如果有一个消费者消费的比较慢就会违背数据库操作的黄金法则(不要保持一个太久的数据库连接)。一般来说,过低的数据库连接数和较长的数据库连接会明显的降低应用的并发性和效应效率,所以这里推荐使用Uni<?>来包装返回结果,如果数据量过大,可以考虑使用分页来解决问题。

  • POST

查看添加的POST请求代码,根据JAX-RS的规范没有提供一种类似于Spring @RequestBody的注解,默认会以JSON类型接收POST请求。

@POST
@Path("create")
public Uni<Response> create(Employee Employee) {
    return Panache.<Employee>withTransaction(Employee::persist)
        .onItem()
        .transform(inserted -> Response.created(URI.create("/Employees/" + inserted.id))
                   .build());
}

为了对数据库进行写操作,需要开启一个数据库事务,Panache.withTransaction会异步的去获取一个事务,当接收到transaction时会调用persist方法,同样persist会返回一个异步Uni结果,Uni会分发Employee在数据库的插入结果,当插入动作完成时(我们编码的延续),我们创建一个201 CREATED响应。在等待接收事务的过程中,RESTEasy Reactive会自动的读取请求体为JSON并且创建一个Employee实体对象。

命令式与反应式

参考官方文档上面的例子简单的示例了反应式编程,在使用上几乎和传统命令式编程没有区别,你可能会疑惑反应式编程和命令式编程有哪些不同或者有哪些好处。为了更好的理解和对比,我们需要先了解反应式和命令式在执行模型上的不同,理解执行模型的是理解Reactive的必要前提。

  • blocking I/O

在传统的命令式编程中,依赖阻塞式I/O模型,框架会分配一个线程去处理一个请求,请求的整个过程都在这个线程上进行,这种模型非常不适合大规模扩展,为了处理大量的请求就需要大量的线程,应用的并发性能会受限于工作线程的数量,当需要与远程服务进行交互调用时候,这些线程就会被阻塞。并且每个线程都映射到 OS 线程,因此在内存和 CPU 方面都有成本,大大降低了资源的利用率。
blocking-threads.png
blocking I/O

  • non-blocking I/O

反应式编程依赖非阻塞式I/O并且使用不同的执行模型,non-blocking I/O提供了一种高效的方式来处理并发I/O,很小一部分I/O线程可以处理大量的并发I/O,通过这种模型,处理请求不会委托给工作线程,而是直接使用这些 I/O 线程。这种模型节省了内存和 CPU,因为不需要创建工作线程来处理请求,提高了并发性,并且消除了对线程数量的限制,由于减少了线程切换的数量,它还提高了响应时间。
reactive-thread.png

  • 顺序性到延续性

命令式体现在顺序性,反应式体现在延续性,是两种本质风格的转变,二者在模型上最大的差别体现在,反应式编程的请求是由I/O线程来处理的,少量的线程即可处理大量的并发请求。
延续性风格编码在处理请求过程需要与远程服务(如 HTTP API 或数据库)交互时,它不会阻塞执行等待响应结果返回,相反,它会调度 I/O 操作并延续处理请求的剩余代码。 这种延续可以作为回调(使用 I/O 结果调用的函数)传递,或者使用更高级的结构体,例如反应式编程或协程。 不管延续如何表达,重要的是对I/O 线程的释放,因此该线程可用于处理另一个请求。 当调度的 I/O 完成时,I/O 线程执行继续,并且继续处理挂起的请求。
因此,与 I/O 阻塞执行的命令式模型不同,反应式切换到基于延续的设计,其中 I/O 线程被释放,并在 I/O 完成时调用延续。 因此,I/O 线程可以处理多个并发请求,从而提高应用程序的整体并发性。

Quarkus提供了不同的反应式编程库:

  • Mutiny - an intuitive and event-driven reactive programming library
  • Kotlin co-routines - a way to write asynchronous code in a sequential manner

参考文档:

更多资料:

源码

https://gitee.com/starsray/quarkus

posted @ 2022-01-17 14:16  星光Starsray  阅读(1788)  评论(0编辑  收藏  举报