Appearance
第44讲:使用Quarku开发微服务
云原生技术的出现,为微服务架构的应用带来了在实现技术选型上的灵活性。使用 Kubernetes 和容器化技术,每个微服务可以选择最适合的技术栈,不限定特定的编程语言或平台。以 Java 来说,任何可以开发 Java 应用的框架和库都可以使用。不过,云原生应用有其特殊的运行需求,主要体现在应用的打包体积、启动时间和运行时的消耗上。
云原生应用以容器镜像的形式来打包,更小的镜像尺寸,就意味着更少的存储空间和更快的传输速度。虽然现在存储空间相对比较廉价,但是考虑到每次构建过程都会产生不可变的容器镜像,在运行时需要保存的容器镜像数量是很多的。每个 Git 提交都有对应的容器镜像。如果能够尽可能地减少单个应用的镜像的尺寸,累积下来所节省的空间是很可观的。
云原生应用的启动速度应该尽可能地快,这是为了满足故障恢复和水平扩展的要求。Kubernetes 经常需要启动新的 Pod,更快的启动速度意味着更快速的响应时间。云原生应用需要共享集群上的资源,单个应用所耗费的资源当然越少越好。
开发框架
在云原生应用的开发中,我们可以把 Java 应用的框架大致分成两类:第一类是传统的 Java 应用框架,以 Spring Boot 为例;第二类是专门为云原生和微服务设计的 Java 应用框架,以 Quarkus、Micronaut、Helidon、Eclipse MicroProfile 为代表,这些新兴框架的特点在于高度的模块化。这一点符合微服务的基本特征,那就是每个微服务只专注于实现特定的功能。通过模块化,每个微服务在实现时,只需要选择特定的模块即可,这可以减少服务所依赖的第三方库的数量,从而减少打包的尺寸。
提到云原生 Java 应用的开发,就必须要提到 GraalVM,这是 Oracle 开发的支持多语言的虚拟机平台,其所支持的编程语言包括 Java、Kotlin 和 Scala 这样的 JVM 语言,也包括 C/C++、JavaScript、Ruby 和 Python 等。对云原生应用来说,GraalVM 的一个重要功能是可以把 Java 应用打包成可执行的原生镜像,从而减少应用的打包尺寸,加快启动速度和减少运行时的资源消耗,这是一个为云原生应用量身定做的功能。是否支持 GraalVM,已经成为衡量云原生微服务开发框架的重要指标。
下面介绍如何使用 Quarkus 框架来实现地址管理服务。
Quarkus 框架
下面对 Quarkus 框架进行介绍。
创建应用
最简单的创建 Quarkus 应用的方式是使用 Quarkus 提供的 Maven 插件。在下面的代码中,通过运行 Maven 插件的 create 目标来创建新的 Quarkus 应用的骨架代码。
java
$ mvn io.quarkus:quarkus-maven-plugin:1.6.1.Final:create \
-DprojectGroupId=io.vividcode.happyride \
-DprojectArtifactId=happyride-address-service-quarkus \
-DprojectVersion=1.0.0-SNAPSHOT
Quarkus 的模块化以扩展为单位,应用可以根据需要添加不同类型的扩展。通过下面的命令可以列出来目前 Quarkus 所支持的全部扩展。
java
$ mvn quarkus:list-extensions
通过 Maven 插件的 add-extension 目标可以添加新的扩展,如下所示。与 add-extension 相对应的 remove-extension 目标可以删除扩展,扩展的标识符可以从 list-extensions 目标的输出中获得。
java
$ mvn quarkus:add-extension -Dextensions="quarkus-flyway, quarkus-resteasy, quarkus-resteasy-jackson, quarkus-hibernate-orm-panache, quarkus-jdbc-postgresql"
与扩展相关的命令本质上只是对 Maven 项目的 POM 文件进行修改,来添加或删除相关的依赖。我们也可以不使用这些命令,而直接修改 POM 文件本身,所产生的效果是一样的。
依赖注入
在 Java 应用的开发中,依赖注入是一个不可或缺的功能,可以极大地简化代码的编写,Spring 框架实现了自己的控制反转容器和依赖注入支持。Quarkus 的依赖注入功能基于 JSR 365:Contexts and Dependency Injection for Java 2.0,也就是 CDI 2.0 规范,不过 Quarkus 仅实现了 CDI 2.0 规范中的部分内容,在绝大部分情况下已经足够了。
依赖注入分为 Bean 的创建和使用两个部分。在创建 Bean 时,我们可以使用 CDI 提供的注解来创建不同作用域的 Bean,如下表所示。
注解 | 作用域 |
---|---|
@ApplicationScoped | 与整个应用的上下文绑定 |
@SessionScoped | 与当前的用户会话上下文绑定 |
@RequestScoped | 与当前的请求上下文绑定 |
在下面的代码中,AddressService 类型的 Bean 出现在应用上下文中。
java
@ApplicationScoped
public class AddressService {
}
在使用 Bean 时,通过 @Inject 注解来进行声明,可以注入的方式包括字段、构造方法和 Setter 方法。在下面的代码中,以字段的方式注入了 AddressService 类型的 Bean。在使用字段的方式注入依赖时,不建议使用 private 作为字段的可见性,因为这会对原生镜像的生成产生影响。使用 package private 可见性是推荐的做法。
java
public class AddressResource {
@Inject
AddressService addressService;
}
Quarkus 默认提供了 3 种不同的概要文件,分别是 dev、test 和 prod。通过 @IfBuildProfile 注解可以限定一个 Bean 只在特定的概要文件中出现。在下面的代码中,AddressLoader 类的 Bean 只在 dev 概要文件中出现。
java
@ApplicationScoped
@IfBuildProfile("dev")
public class AddressLoader {
}
数据访问
在 Quarkus 中,我们可以用 Hibernate 来访问关系型数据库,这也是 Java 应用中访问关系型数据库的通用做法。 在应用中可以通过标准的方式来使用 Hibernate,也就是使用依赖注入的 EntityManager 对象来进行实体的持久化。不过更好的做法是使用 Panache 库,它在很大程度上简化了对 Hibernate 的使用。在使用 Panache 之前,需要在 Quarkus 应用中添加相关的扩展。
Panache 支持两种不同的使用模式:第一种使用活动记录(Active Record)模式 ,把数据查询的逻辑添加在实体类中;第二种是使用仓库(Repository)模式 ,把数据查询的逻辑添加在仓库类,这也是 Spring Data JPA 使用的模式。由于第 11 课时已经介绍了仓库模式的使用,本课时介绍活动记录模式。
下面代码中的 Address 是 Panache 中的实体类。Address 的父类 PanacheEntityBase 提供了与实体的 LCRUD 操作相关的方法,包括 list、find、update 和 delete 等。Address 类使用标准的 JPA 注解来描述实体和关系。Address 类使用 String 类型的标识符,因此需要自定义的 id 字段。如果你的实体使用自动生成的 Long 类型的标识符,那么可以直接继承自 PanacheEntity 类,而不需要添加额外的 id 字段。
除了字段的声明之外,Address 类还包含了一个静态方法 findByAreaCodeAndAddressLine,用来根据 areaCode 和 query 进行查找,这里用到了 Hibernate 的 HQL 来进行查询。
java
@Entity
@Table(name = "addresses")
public class Address extends PanacheEntityBase {
@Id
public String id;
@ManyToOne
@JoinColumn(name = "area_id")
public Area area;
@Column(name = "address_line")
@Size(max = 255)
public String addressLine;
@Column(name = "lng")
public BigDecimal lng;
@Column(name = "lat")
public BigDecimal lat;
public static List<Address> findByAreaCodeAndAddressLine(Long areaCode,
String query) {
return list("area.areaCode = :areaCode and addressLine LIKE :query",
ImmutableMap.of(
"areaCode", areaCode,
"query", "%" + query + "%"
));
}
}
服务层
服务层的实现类使用实体类来进行数据查询。下面代码中的 AddressService 类是进行地址查询的服务实现,该服务类通过 @Transactional 注解启用了事务支持。
java
@ApplicationScoped
@Transactional
public class AddressService {
public List<AddressVO> search(Long areaCode, String query) {
return Address.findByAreaCodeAndAddressLine(areaCode, query)
.stream()
.map(AddressHelper::fromAddress)
.collect(Collectors.toList());
}
}
REST 服务
在 Quarkus 中,可以使用 resteasy 扩展来发布 REST 服务。与 Spring MVC 不同的是,Resteasy 使用 JAX-RS 的注解来对 REST 资源进行声明,在进行 JSON 序列化时,可以选择 JSON-B 或 Jackson,只需要添加对应的扩展即可。
下面代码中的 AddressResource 类是地址资源的实现,其中以依赖注入的方式使用服务层实现类 AddressService。
java
@Path("/")
public class AddressResource {
@Inject
AddressService addressService;
@Path("/search")
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<AddressVO> search(
@QueryParam("areaCode") Long areaCode,
@QueryParam("query") String query) {
return this.addressService.search(areaCode, query);
}
}
应用配置
配置是 Quarkus 应用不可或缺的一部分。地址管理服务需要通过配置来设置关系式数据库的连接信息。最简单的对 Quarkus 进行配置的方式是编辑 src/main/resources 目录下的 application.properties 文件,如下面的代码所示,该配置文件中包含对应于不同扩展的配置项。配置项的名称以 a.b.c 的形式来表示,相互关联的配置项具有相同的前缀,比如,quarkus.flyway 前缀表示 Flyway 相关的配置。
java
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=${DB_USERNAME:postgres}
quarkus.datasource.password=${DB_PASSWORD:postgres}
quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:8430}/${DB_NAME:happyride-address}
quarkus.hibernate-orm.database.default-schema=happyride
quarkus.flyway.migrate-at-start=true
quarkus.flyway.schemas=happyride
除了 Quarkus 中扩展的配置项之外,应用也可以添加自定义的配置项。在应用中,使用配置项最简单的方式是添加 @ConfigProperty 注解。在下面的代码中,value 的值与配置项 app.value 进行绑定。
java
@ConfigProperty(name = "app.value")
String value;
Quarkus 同样支持 Spring Boot 中的类型安全的配置类。下面代码中的配置类 AppConfiguration 使用 @ConfigProperties 注解与前缀为 app 的配置项进行绑定。配置类中的字段与同名的配置项进行绑定。
java
@ConfigProperties(prefix = "app")
public class AppConfiguration {
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
当需要获取配置时,只需要以依赖注入的形式来获取 AppConfiguration 类的对象即可。
java
@Inject
AppConfiguration appConfiguration;
除了属性文件之外,Quarkus 也支持 YAML 格式的配置文件,需要 config-yaml 扩展的支持。
单元测试
Quarkus 提供了对单元测试的支持。在单元测试中,可以分别对数据访问层、服务层和 REST 资源进行测试。Quarkus 也支持使用 Mockito 来模拟对象。
下面代码中的 AddressResourceTest 类是 AddressResource 的单元测试用例。@QuarkusTest 注解的作用是声明 Quarkus 测试类。在 @BeforeAll 注解声明的初始化方法中,我们使用 Mockito 来模拟 AddressResource 类中用到的 AddressService 和 AreaService 对象,其中模拟的 AddressService 对象的 search 方法,总是返回包含单个特定的 AddressVO 对象的列表。QuarkusMock 的 installMockForType 方法用来注册 Mock 对象。
在 testSearch 方法中,我们通过 REST Assured 库来发送请求到 REST 资源,并验证 HTTP 响应的状态码和内容,返回的 JSON 数组中应该只包含一个元素。
java
@QuarkusTest
public class AddressResourceTest {
@BeforeAll
public static void setup() {
AddressService addressService = Mockito.mock(AddressService.class);
Mockito.when(addressService.search(anyLong(), anyString())).thenReturn(
Collections.singletonList(createAddress()));
QuarkusMock.installMockForType(addressService, AddressService.class);
QuarkusMock
.installMockForType(Mockito.mock(AreaService.class), AreaService.class);
}
@Test
public void testSearch() {
given()
.when()
.queryParam("areaCode", "1")
.queryParam("query", "test")
.get("/search")
.then()
.statusCode(200)
.body("$", hasSize(1));
}
private static AddressVO createAddress() {
AddressVO address = new AddressVO();
address.setId(UUID.randomUUID().toString());
address.setAreaId(0);
address.setAddressLine("Test");
address.setLat(BigDecimal.ZERO);
address.setLng(BigDecimal.ONE);
return address;
}
}
本地开发
在本地开发中,我们可以通过下面的命令来启动 Quarkus 应用。
java
$ mvn quarkus:dev
通过这种方式启动的Quarkus应用运行在开发模式,会应用概要文件 dev。在开发模式中,Quarkus支持热部署。在修改了 Java 代码或资源文件之后,当刷新浏览器时,Quarkus 会重新编译 Java 文件,并重新部署应用。开发人员并不需要重启服务器,就可以查看更新之后的结果,这种开发模式,可以极大地提升开发效率。
应用打包
使用 mvn package 命令可以对 Quarkus 应用进行打包。在运行完该命令之后,除了标准的 JAR 文件之外,还会生成一个带 runner 后缀的 JAR 文件,用来启动应用。运行应用所需的第三方依赖导出在 lib 目录中。在发布应用时,只需要把带 runner 后缀的 JAR 文件和 lib 目录复制到同一个目录下,并使用 java -jar 命令来运行 JAR 文件即可。
在云平台上运行时,我们需要创建 Quarkus 应用的容器镜像。在 Quarkus 应用的骨架代码中,Maven 项目的 src/main/docker 目录中已经包含了 3 个 Dockerfile,如下表所示。
Dockerfile | 说明 |
---|---|
Dockerfile.jvm | 在 JVM 中运行,使用带 runner 后缀的 JAR 文件 |
Dockerfile.fast-jar | 在 JVM 中运行,使用 fast-jar 的打包格式 |
Dockerfile.native | 打包成 GraalVM 的原生镜像 |
fast-jar 是 Quarkus 1.5 中引入的新打包格式,可以提升启动速度。打包的应用会出现在 target/quarkus-app 目录下,需要在配置文件中添加额外的选项来启用这种打包方式,如下所示。
java
quarkus.package.type=fast-jar
通过下面的命令可以构建出相应的容器镜像。
java
$ docker build -f src/main/docker/Dockerfile.fast-jar -t quarkus/happyride-address-service-quarkus-jvm .
原生镜像
在创建原生镜像之前,首先要安装 GraalVM。我们可以从 GraalVM 的 GitHub 上下载 GraalVM 的社区版,也可以通过 Homebrew 或 SDKMAN 这样的工具来安装,安装版本为 20.1.0。安装之后,需要配置环境变量 GRAALVM_HOME 来指向安装目录。
GraalVM 的原生镜像生成工具是一个可选的组件,需要手动安装,可使用下面的命令来安装组件 native-image。
java
${GRAALVM_HOME}/bin/gu install native-image
使用下面的命令可以创建出 Quarkus 应用的可执行文件,quarkus.native.container-build=true 选项的作用是创建适用于容器运行的原生可执行文件。需要注意的是,创建原生镜像需要较大的内存资源,并且比较耗时,需要确保 Docker 运行时有足够的内存资源。
java
$ mvn package -Pnative -Dquarkus.native.container-build=true
接着使用下面的命令来创建容器镜像。
java
$ docker build -f src/main/docker/Dockerfile.native -t quarkus/happyride-address-service-quarkus-native .
下面的代码给出了 docker images 命令的输出,从中可以看到,使用 GraalVM 原生镜像的容器镜像的尺寸只有 182 M,远小于使用 JVM 的容器镜像。除了容器镜像的尺寸之外,原生镜像的容器的启动速度也更快。
java
REPOSITORY SIZE
quarkus/happyride-address-service-quarkus-native 182MB
quarkus/happyride-address-service-quarkus-jvm 533MB
总结
在云原生微服务架构应用的开发中,我们可以使用不同的 Java 微服务开发框架,并不仅限于 Spring Boot。通过本课时的学习,你可以了解到微服务开发框架 Quarkus,以及如何用 Quarkus 来开发和测试微服务,并构建出原生镜像,从而提高微服务的启动速度。
最后呢,成老师邀请你为本专栏课程进行结课评价,因为你的每一个观点都是我们最关注的点。点击链接,即可参与课程评价。