Helidon项目教程:如何使用Oracle轻量级Java框架构建微服务

本文要点:

  • Helidon是由Oracle在2018年9月份推出的轻量级微服务框架。
  • Helidon是一个创建微服务应用的Java库的集合。
  • 按照设计,Helidon非常简单和快捷,它提供了两个版本:Helidon SE和Helidon MP。
  • Helidon支持GraalVM,能够将Helidon SE应用转换为原生可执行的代码。
  • 在本教程中,我们将会向你介绍Helidon,探索Helidon SE和Helidon MP,并且会下载本教程相关的GitHub仓库内容。
  • Helidon 1.4.4是当前的稳定版本,不过Helidon 2.0计划在今年发布。

2018年9月份,Oracle推出了新的开源框架Helidon项目。Helidon最初的名字叫做J4C(Java for Cloud),它是一个创建基于微服务应用的Java库的集合。在推出六个月之后,Helidon 1.0于2019年2月份发布。目前的稳定发布版本是Helidon 1.4.4,但是Oracle正在计划发布Helidon 2.0(2.0版本已经在6月25日发布,参见发布声明变更记录——译者注)。

本教程将会介绍Helidon SE和Helidon MP,探索Helidon SE的三个核心组件、如何起步并且还会介绍一个基于Helidon MP构建的电影应用。另外,我们还有关于GraalVM的讨论以及在即将发布的Helidon 2.0中都有哪些值得期待的功能。

Helidon 概览

按照设计,Helidon 非常简单和快捷,它很独特的一点在于它提供了两个编程模型Helidon SEHelidon MP。在下图中,展示与其他流行的微服务框架对比,Helidon SE 和 Helidon MP 分别位于什么地方。

Helidon SE

Helidon SE 是一个微框架,它提供了创建微服务的三个核心组件来构建基于微服务的应用,即 Web 服务器、配置以及安全性。它是一个很小的函数式 API,具有反应式(reactive)、简单和透明的特点,不需要应用服务器。

我们通过一个简单的例子看一下函数式风格的 Helidon SE,这里使用 WebServer 接口启动了一个 Helidon Web 服务器:

WebServer.create(
    Routing.builder()
        .get("/greet", (req, res)
             -> res.send("Hello World!"))
        .build())
    .start();

以该例子作为起点,我们将会增量式地构建一个正式的startServer()方法,以此探索 Helidon SE 的三个核心组件,它是你所下载的服务器应用的一部分。

Web 服务器组件

受到 NodeJS 和其他 Java 框架灵感的启发,Helidon 的 web 服务器组件是一个运行在 Netty 上的异步反应式 API。WebServer接口提供了基本的服务器生命周期和监控,可以通过配置、路由、错误处理以及构建度量指标和健康端点来进行增强。

我们从startServer()方法的第一个版本开始,它会在一个随机可用的端口上启动 Helidon web 服务器:

private static void startServer() {
    Routing routing = Routing.builder()
            .any((request, response) -> response.send("Greetings from the web server!" + "\n"))
            .build();

    WebServer webServer = WebServer
            .create(routing)
            .start()
            .toCompletableFuture()
            .get(10, TimeUnit.SECONDS);

    System.out.println("INFO: Server started at: http://localhost:" + webServer.port() + "\n");
    }

首先,我们需要构建一个 Routing 接口的实例,它会作为一个具有路由规则的 HTTP 请求 - 响应处理器。在本例中,我们使用any()方法将请求路由至定义好的服务器响应”Greetings from the web server!“,这个响应信息会通过浏览器或者curl命令展示出来。

在构建 web 服务器的时候,我们调用了重载的create()方法。按照设计,该方法用来接受各种服务器配置。如上所示,最简单的方式就是接受我们刚刚创建的用来提供默认服务器配置的实例变量routing

按照设计,Helidon Web 服务器是反应式的,这意味着start()方法会返回一个 CompletionStage<WebServer> 接口的实例来启动 Web 服务器。它允许我们调用toCompletableFuture()方法。因为这里没有指定服务器端口,服务器在启动的时候会选择任意一个可用的端口。

接下来,我们使用 Maven 构建并运行我们的服务器应用:

$ mvn clean package
$ java -jar target/helidon-server.jar

服务器启动的时候,我们会在终端窗口看到如下所示的输出:

Apr 15, 2020 1:14:46 PM io.helidon.webserver.NettyWebServer &lt;init&gt;
INFO: Version: 1.4.4
Apr 15, 2020 1:14:46 PM io.helidon.webserver.NettyWebServer lambda$start$8
INFO: Channel '@default' started: [id: 0xcba440a6, L:/0:0:0:0:0:0:0:0:52535]
INFO: Server started at: http://localhost:52535

如最后一行所示,Helidon web 服务器选择了 52535 端口。在服务器运行的时候,在浏览器输入这个 URL 或者在单独的终端窗口执行如下的curl命令:

$ curl -X GET http://localhost:52535

我们将会看到“Greetings from the web server!

要关闭 web 服务器,我们只需要添加如下这行代码:

webServer.shutdown()
        .thenRun(() -> System.out.println("INFO: Server is shutting down...Good bye!"))
        .toCompletableFuture();

配置组件

配置组件会加载和处理配置属性。Helidon 的 Config 接口将会从预先定义的配置文件中读取配置属性,配置文件通常是YAML 格式,但是并不限于此。

我们创建一个application.yaml文件,它提供了应用、服务器和安全性方面的配置。

app:
  greeting: "Greetings from the web server!"

server:
  port: 8080
  host: 0.0.0.0

security:
  config:
    require-encryption: false

  providers:
    - http-basic-auth:
        realm: "helidon"
        users:
          - login: "ben"
            password: "${CLEAR=password}"
            roles: ["user", "admin"]
          - login: "mike"
            password: "${CLEAR=password}"
            roles: ["user"]
    - http-digest-auth:

application.yaml文件中有三个主要的组成部分或者说是节点,即appserversecurity。前两个节点非常简单直接。greeting子节点定义了我们在上述样例中硬编码的服务器响应。port子节点定义了 web 服务器在启动的时候所使用的端点是 8080。但是,你应该也注意到了,security节点要复杂一些,它使用 YAML 的映射序列定义了多个条目。通过使用“-”字符分割,我们定义了两个安全 provider(即http-basic-authhttp-digest-auth)和两个用户(即benmike)。在本教程的安全组件章节,我们将会对其进行详细讨论。

另外,这个配置允许我们通过将config.require-encryption设置为false以使用明文密码。在生产环境中,我们显然需要将这个值设置为true,这样的话试图传入明文密码会抛出异常。

现在,基于这个可用的配置文件,我们就可以更新startServer()方法来使用刚刚定义的配置。

private static void startServer() {
    Config config = Config.create();
    ServerConfiguration serverConfig = ServerConfiguration.create(config.get("server"));

    Routing routing = Routing.builder()
            .any((request, response) -> response.send(config.get("app.greeting").asString().get() + "\n"))
            .build();

    WebServer webServer = WebServer
            .create(serverConfig, routing)
            .start()
            .toCompletableFuture()
            .get(10, TimeUnit.SECONDS);

    System.out.println("INFO: Server started at: http://localhost:" + webServer.port() + "\n");
    }

首先,我们需要通过调用Config create() 方法构建该接口的一个实例。Config提供的get(String key)方法能够返回配置文件中给定key所声明的节点或子节点。例如,config.get("server") 将会返回server节点下的内容,config.get("app.greeting") 将会返回“Greetings from the web server!”。

接下来,我们创建了 ServerConfiguration 实例并为其提供不可变的 web 服务器信息,这是通过调用其create()方法并传入 config.get("server") 语句实现的。

实例变量routing的构造方式和之前的样例很相似,只不过我们消除了硬编码的服务器响应,将其替换为调用 config.get("app.greeting").asString().get()。

Web 服务器的创建过程和之前的样例类似,只不过我们使用了一个不同版本的create()方法,它接受两个实例变量serverConfigrouting

我们可以使用相同的 Maven 和 Java 命令来构建和运行这个版本的 Web 服务器应用。执行相同的curl命令:

$ curl -X GET http://localhost:8080

你应该会看到“Greetings from the web server!

安全组件

Helidon 的安全组件提供了认证、授权、审计和出站安全性功能。在 Helidon 应用中,支持使用大量的安全供应商实现:

  • HTTP Basic 认证
  • HTTP Digest 认证
  • HTTP 签名
  • 基于属性的访问控制(Attribute Based Access Control,ABAC)授权
  • JWT Provider
  • Header 断言
  • Google 登录认证
  • OpenID Connect
  • IDCS 角色映射(IDCS Role Mapping)

在 Helidon 应用中,我们可以采用如下三种方式的一种来实现安全性:

  • 手动提供配置的构建者模式
  • 提供配置文件的配置模式
  • 组合使用构建者模式和配置模式的混合模式

在样例应用中,我们将会采用混合方式,但是我们首先要做一些准备工作。

我们看一下如何引用在配置文件中 security 节点下所定义的用户。考虑如下的字符串:

security.providers.0.http-basic-auth.users.0.login

当解析器遇到字符串中的数字时,就意味着配置文件中有一个或多个子节点。在本例中,providers后面的0将会指导解析器转移至第一个 provider 子节点,即http-basic-authusers后面的0将会指导解析器转移至包含loginpasswordroles的第一个 user 子节点。因此,当传递到config.get()方法时,上述的字符串将会返回ben用户的 login、password 和 role 信息。与之类似,mike用户的 login、password 和 role 信息可以通过如下的字符串获取到:

security.providers.0.http-basic-auth.users.1.login

接下来,我们为 Web 服务器应用创建一个新的类AppUser,它实现了 SecureUserStore.User 接口:

public class AppUser implements SecureUserStore.User {

    private String login;
    private char[] password;
    private Collection<String> roles;

    public AppUser(String login, char[] password, Collection<String> roles) {
        this.login = login;
        this.password = password;
        this.roles = roles;
        }

    @Override
    public String login() {
        return login;
        }

    @Override
    public boolean isPasswordValid(char[] chars) {
        return false;
        }

    @Override
    public Collection<String> roles() {
        return roles;
        }

    @Override
    public Optional<String> digestHa1(String realm, HttpDigest.Algorithm algorithm) {
        return Optional.empty();
        }
    }

我们将会使用这个类来构建角色和用户的 map,如下所示:

Map<String, AppUser> users = new HashMap<>();

为了实现这一点,我们为 Web 服务器应用添加了一个新的方法getUsers(),它会使用配置文件中http-basic-auth子元素的配置来填充这个 map。

private static Map<String, AppUser> getUsers(Config config) {
    Map<String, AppUser> users = new HashMap<>();

    ConfigValue<String> ben = config.get("security.providers.0.http-basic-auth.users.0.login").asString();
    ConfigValue<String> benPassword = config.get("security.providers.0.http-basic-auth.users.0.password").asString();
    ConfigValue<List<Config>> benRoles = config.get("security.providers.0.http-basic-auth.users.0.roles").asNodeList();

    ConfigValue<String> mike = config.get("security.providers.0.http-basic-auth.users.1.login").asString();
    ConfigValue<String> mikePassword = config.get("security.providers.0.http-basic-auth.users.1.password").asString();
    ConfigValue<List<Config>> mikeRoles = config.get("security.providers.0.http-basic-auth.users.1.roles").asNodeList();

    users.put("admin", new AppUser(ben.get(), benPassword.get().toCharArray(), Arrays.asList("user", "admin")));
    users.put("user", new AppUser(mike.get(), mikePassword.get().toCharArray(), Arrays.asList("user")));

    return users;
    }

我们为 Web 服务器应用准备好了这个新功能,接下来,我们更新startServer()方法,使用 Helidon 的 HTTP Basic 认证实现来为其添加安全性:

private static void startServer() {
    Config config = Config.create();
    ServerConfiguration serverConfig = ServerConfiguration.create(config.get("server"));

    Map<String, AppUser> users = getUsers(config);
    displayAuthorizedUsers(users);

    SecureUserStore store = user -> Optional.ofNullable(users.get(user));

    HttpBasicAuthProvider provider = HttpBasicAuthProvider.builder()
            .realm(config.get("security.providers.0.http-basic-auth.realm").asString().get())
            .subjectType(SubjectType.USER)
            .userStore(store)
            .build();

    Security security = Security.builder()
            .config(config.get("security"))
            .addAuthenticationProvider(provider)
            .build();

    WebSecurity webSecurity = WebSecurity.create(security)
            .securityDefaults(WebSecurity.authenticate());

    Routing routing = Routing.builder()
            .register(webSecurity)
            .get("/", (request, response) -> response.send(config.get("app.greeting").asString().get() + "\n"))
            .get("/admin", (request, response) -> response.send("Greetings from the admin, " + users.get("admin").login() + "!\n"))
            .get("/user", (request, response) -> response.send("Greetings from the user, " + users.get("user").login() + "!\n"))
            .build();

    WebServer webServer = WebServer
            .create(serverConfig, routing)
            .start()
            .toCompletableFuture()
            .get(10, TimeUnit.SECONDS);

    System.out.println("INFO: Server started at: http://localhost:" + webServer.port() + "\n");
    }

和前面样例做法一样,我们构建了变量实例configserverConfig。随后,我们通过上述的getUsers()方法构建了角色和用户的 map。

这里利用了 Optional 的空类型安全性,store实例变量是通过 SecureUserStore 接口构建的,如 lambda 表达式所示。SecureUserStore 同时用于 HTTP Basic 认证和 HTTP Digest 认证。需要注意,HTTP Basic 可能是非安全的,即便使用 SSL 也是如此,因为密码并不是必需的。

我们现在已经准备好构建 HTTPBasicAuthProvider 实例了,它是 SecurityProvider 接口的一个实现类。realm()方法定义了在未认证的时候发送至浏览器(或其他客户端)的安全 realm 名。因为我们在配置文件中定义了一个 realm,所以我们将它传递到了该方法中。subjectType()方法定义了安全 provider 抽取或传播的 principal 类型。它会接受SubjectType 枚举的两个值中的一个,也就是USERSERVICEuserStore()方法接受我们刚刚构建的store实例变量,用来在我们的应用中校验用户。

借助provider实例变量,我们现在就可以构建 Security 类的实例了,用来启动安全功能并将它与其他框架进行集成。我们使用config()addAuthenticationProvider()来完成这一点。需要注意的是,我们可以注册多个安全 provider,只需要通过addAuthenticationProvider()方法将它们链接在一起即可。例如,假设我们定义了实例变量basicProviderdigestProvider,它们分别代表HttpBasicAuthProvider HttpDigestAuthProvider 类,那么我们的security实例变量可以按照如下的方式进行构建:

Security security = Security.builder()
        .config(config.get("security"))
        .addAuthenticationProvider(basicProvider)
        .addAuthenticationProvider(digestProvider)
        .build();

WebSecurity 类实现了 Service 接口,它封装了一组路由规则和相关逻辑。实例变量webSecurity是通过create()方法和securityDefaults()方法构建的,前者将security 实例变量传递了进去而后者则将WebSecurity.authentic()传递了进去,从而确保请求将会经过认证过程。

我们熟悉的实例变量routing并没有太大的差异,在前面两个样例中我们已经构建过它。它注册了webSecurity实例变量并定义了端点“/”、“/admin”和“/user”,这是通过get()方法将它们链接起来的。注意,/admin/user端点分别关联了benmike

最后,我们的 web 服务器就可以启动了!在实现了所有的零部件之后,构建 web 服务器就和之前的样例完全一样了。

现在,我们可以使用相同的 Maven 和 Java 命令构建和运行 web 服务器应用了,执行如下的curl命令:

$ curl -X GET [http://localhost:8080/](http://localhost:8080/)将会返回“Greetings from the web server!

$ curl -X GET [http://localhost:8080/admin](http://localhost:8080/admin)将会返回“Greetings from the admin, ben!

$ curl -X GET [http://localhost:8080/user](http://localhost:8080/user)将会返回“Greetings from the user, mike!

你可以看到阐述所有三个版本startServer()方法的综合服务器应用,它们关联了我们刚刚探讨的三个Helidon SE 核心组件。同时,你也可以参考更广泛的安全样例,它们会为你展示如何实现其他的安全provider。

Helidon MP

Helidon MP 构建在 Helidon SE 之上,是一个小型的、声明式风格的 API,它是 MicroProfile 规范的实现, MicroProfile 是一个平台,致力于将企业级 Java 优化为微服务架构,适用于构建基于微服务的应用。MicroProfile 最初是由 IBM、Red Hat、Payara 和 Tomitribe 在 2006 年联合成立的,当时它定义了最初的三个 API,即 CDI ( JSR 365 )、JSON-P (JSR 374 ) 和 JAX-RS ( JSR-370 ),它们被认为是构建微服务应用所需的最小数量的 API。从那时开始,MicroProfile 已经发展到 12 个核心 API 以及支持反应式流和 GraphQL 的 4 个独立 API。MicroProfile 3.3 于 2020 年 2 月发布,是最新的版本。

Helidon MP 目前支持 MicroProfile 3.2。对于 Java EE/Jakarta EE 开发人员来说,Helidon MP 是一个非常好的可选方案,因为它使用注解实现了熟悉的声明式方式。它不需要特殊的部署模型,也不需要额外的 Java EE 打包。

我们看一下 Helidon MP 的声明式风格,这是一个启动 Helidon web 的简单样例,可以将其与 Helidon SE 的函数式风格进行一下对比。

public class GreetService {
  @GET
  @Path("/greet")
  public String getMsg() {
    return "Hello World!";
    }
  }

请注意这种风格与 Helidon SE 函数式风格的差异。

Helidon 架构

既然已经介绍了 Helidon SE 和 Helidon MP,那么我们看一下它们是如何组合在一起的。Helidon 的架构如下图所示。Helidon MP 是构建在 Helidon SE 和 CDI 扩展之上的,如下文所述,CDI 扩展丰富了 Helidon MP 的云原生能力。

CDI Extensions

Helidon 提供了可迁移的上下文与依赖注入(Context and Dependency Injection,CDI)扩展,它支持与各种数据源、事务和客户端进行集成,扩展 Helidon MP 应用的云原生功能。目前它提供了如下的扩展:

Helidon 快速上手指南

Helidon 为 Helidon SE Helidon MP 都提供了快速上手指南。我们只需要访问这些页面并遵循指令即可。例如,我们可以通过在终端窗口执行如下的 Maven 命令就能快速构建一个 Helidon SE 应用:

$ mvn archetype:generate -DinteractiveMode=false \
    -DarchetypeGroupId=io.helidon.archetypes \
    -DarchetypeArtifactId=helidon-quickstart-se \
    -DarchetypeVersion=1.4.4 \
    -DgroupId=io.helidon.examples \
    -DartifactId=helidon-quickstart-se \
    -Dpackage=io.helidon.examples.quickstart.se

这将会在helidon-quickstart-se目录下生成一个小型的但是可运行的应用,它包含了测试和各种配置文件,这些配置文件用于应用(application.yaml)、日志 (logging.properties)、使用 GraalVM 构建原生镜像 (native-image.properties)、使用 Docker 容器化应用 (DockerfileDockerfile.native) 以及使用 Kubernetes 进行应用编排 (app.yaml)。

类似地,我们可以快速构建 Helidon MP 应用:

$ mvn archetype:generate -DinteractiveMode=false \
    -DarchetypeGroupId=io.helidon.archetypes \
    -DarchetypeArtifactId=helidon-quickstart-mp \
    -DarchetypeVersion=1.4.4 \
    -DgroupId=io.helidon.examples \
    -DartifactId=helidon-quickstart-mp \
    -Dpackage=io.helidon.examples.quickstart.mp

对于构建复杂的应用来讲,这是一个很好的起点,我们会在下一节讨论一个复杂的应用。

Movie 应用

基于所生成的 Helidon MP 快速上手应用,我们添加了一些额外的类完成了 movie 应用,新增加的类包括 POJO、资源、repository、自定义异常以及 ExceptionMapper 的实现,该应用会维护 Quentin Tarantino 电影的一个列表。HelidonApplication类如下所示,它会注册所需的类。

@ApplicationScoped
@ApplicationPath("/")
public class HelidonApplication extends Application {

    @Override
    public Set<Class<?>> getClasses() {
        Set<Class<?>> set = new HashSet<>();
        set.add(MovieResource.class);
        set.add(MovieNotFoundExceptionMapper.class);
        return Collections.unmodifiableSet(set);
        }
    }

你可以 colne GitHub 仓库以了解关于该应用的详细信息。

GraalVM

Helidon 支持 GraalVM ,它是一个多语言的虚拟机和平台,能够将应用转换成原生可执行代码。GraalVM 是由 Oracle Labs 创建的,由 Graal SubstrateVM Truffle 组成,其中 Graal 是一个使用 Java 编写的即时编译器,SubstrateVM 是一个允许提前将 Java 应用编译为可执行镜像的框架,Truffle 则是一个用于构建语言解释器(interpreter)的开源工具集和 API。它最新的版本是 20.1.0。

我们可以通过 GraalVM 的native-image工具将 Helidon SE 应用转换成原生可执行代码,native-image需要通过 GraalVM 的gu工具单独进行安装:

$ gu install native-image
$ export
GRAALVM_HOME=/usr/local/bin/graalvm-ce-java11-20.1.0/Contents/Home

安装完成之后,我们就可以回到helidon-quickstart-se目录并执行如下的命令:

$ mvn package -Pnative-image

这个操作将会耗时几分钟,完成之后,我们的应用就转换成了原生代码。可执行文件位于/target目录下。

Helidon 2.0 的路线图

Helidon 2.0.0 计划在 2020 年发布(目前,该版本已经发布,读者可以参考该地址——译者注)。该版本重要的新特性包括为Helidon MP 应用添加对GraalVM 的支持、新的Web Client 和 DB Client 组件、新的 CLI 工具以及独立 MicroProfile Reactive Messaging Reactive Streams Operators API 的实现。

直到最近,由于用到了 CDI 2.0( JSR 365 ) 的反射(这是 MicroProfile API 的一个核心 API),所以只有 Helidon SE 应用能够利用 GraalVM。但是,根据客户的需要,Helidon 2.0.0 将支持 Helidon MP 应用转换成原生镜像。Oracle 创建了一个示例应用为Java 社区预览这个新特性。

为了补充原有的三个核心Helidon SE API,即Web 服务器、配置和安全性,新的 Web Client API 完备了 Helidon SE 的特性集。通过构建 WebClient 接口的实例,我们能够处理对特定端点的 HTTP 请求和响应。和 Web Server API 一样,Web Client 也可以通过配置文件进行配置。

我们可以详细了解开发人员可以期待Helidon 2.0.0 会带来哪些新功能。

作者简介:

Michael Redlich 是新泽西州克林顿市 ExxonMobil 研究与工程部门的高级研究技术人员(观点仅代表个人),在过去的 30 年里,他具有开发定制科学实验室和 web 应用的经验。他还曾经在 Ai-Logix, Inc.(现在是 AudioCodes 了)担任过技术支持工程师,为客户提供技术支持和开发电话应用程序。他的技术专长包括面向对象设计和分析、关系型数据库设计和开发、计算机安全、C/ C++、Java、Python 和其他编程 / 脚本语言。他最近的关注点包括 MicroProfile Jakarta EE Helidon Micronaut MongoDB

原文链接:

Project Helidon Tutorial: Building Microservices with Oracle’s Lightweight Java Framework

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/hVNFyOki9UH0lHDjtm91
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券