Skip to content

第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 来开发和测试微服务,并构建出原生镜像,从而提高微服务的启动速度。

最后呢,成老师邀请你为本专栏课程进行结课评价,因为你的每一个观点都是我们最关注的点。点击链接,即可参与课程评价