@levinzhang
2020-07-19T09:16:25.000000Z
字数 14102
阅读 806
by
2018年9月份,Oracle推出了新的开源框架Helidon。Helidon最初的名字叫做Java for Cloud,它是一个创建基于微服务应用的Java库的集合。在推出六个月之后,Helidon 1.0于2019年2月份发布。目前的稳定发布版本是Helidon 1.4.4,但是Oracle正在计划发布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 SE和Helidon MP。在下图中,展示与其他流行的微服务框架对比,Helidon SE和Helidon MP分别位于什么地方。
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的三个核心组件,它是你所下载的服务器应用的一部分。
受到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 <init>
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
文件中有三个主要的组成部分或者说是节点,即app
、server
和security
。前两个节点非常简单直接。greeting
子节点定义了我们在上述样例中硬编码的服务器响应。port
子节点定义了web服务器在启动的时候所使用的端点是8080。但是,你应该也注意到了,security
节点要复杂一些,它使用YAML的映射序列定义了多个条目。通过使用“-
”字符分割,我们定义了两个安全provider(即http-basic-auth
和http-digest-auth
)和两个用户(即ben
和mike
)。在本教程的安全组件章节,我们将会对其进行详细讨论。
另外,这个配置允许我们通过将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()
方法,它接受两个实例变量serverConfig
和routing
。
我们可以使用相同的Maven和Java命令来构建和运行这个版本的Web服务器应用。执行相同的curl
命令:
$ curl -X GET http://localhost:8080
你应该会看到“Greetings from the web server!”
Helidon的安全组件提供了认证、授权、审计和出站安全性功能。在Helidon应用中,支持使用大量的安全供应商实现:
在Helidon应用中,我们可以采用如下三种方式的一种来实现安全性:
在样例应用中,我们将会采用混合方式,但是我们首先要做一些准备工作。
我们看一下如何引用在配置文件中security节点下所定义的用户。考虑如下的字符串:
security.providers.0.http-basic-auth.users.0.login
当解析器遇到字符串中的数字时,就意味着配置文件中有一个或多个子节点。在本例中,providers
后面的0
将会指导解析器转移至第一个provider子节点,即http-basic-auth
。users
后面的0
将会指导解析器转移至包含login
、password
和roles
的第一个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");
}
和前面样例做法一样,我们构建了变量实例config
和serverConfig
。随后,我们通过上述的getUsers()
方法构建了角色和用户的map。
这里利用了Optional的空类型安全性,store
实例变量是通过SecureUserStore接口构建的,如lambda表达式所示。SecureUserStore同时用于HTTP Basic认证和HTTP Digest认证。需要注意,HTTP Basic可能是非安全的,即便使用SSL也是如此,因为密码并不是必需的。
我们现在已经准备好构建HTTPBasicAuthProvider实例了,它是SecurityProvider接口的一个实现类。realm()
方法定义了在未认证的时候发送至浏览器(或其他客户端)的安全realm名。因为我们在配置文件中定义了一个realm,所以我们将它传递到了该方法中。subjectType()
方法定义了安全provider抽取或传播的principal类型。它会接受SubjectType枚举的两个值中的一个,也就是USER
或SERVICE
。userStore()
方法接受我们刚刚构建的store
实例变量,用来在我们的应用中校验用户。
借助provider
实例变量,我们现在就可以构建Security类的实例了,用来启动安全功能并将它与其他框架进行集成。我们使用config()
和addAuthenticationProvider()
来完成这一点。需要注意的是,我们可以注册多个安全provider,只需要通过addAuthenticationProvider()
方法将它们链接在一起即可。例如,假设我们定义了实例变量basicProvider
和digestProvider
,它们分别代表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
端点分别关联了ben
和mike
。
最后,我们的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 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 SE和Helidon MP,那么我们看一下它们是如何组合在一起的。Helidon的架构如下图所示。Helidon MP是构建在Helidon SE和CDI扩展之上的,如下文所述,CDI扩展丰富了Helidon MP的云原生能力。
Helidon提供了可迁移的上下文与依赖注入(Context and Dependency Injection,CDI)扩展,它支持与各种数据源、事务和客户端进行集成,扩展Helidon MP应用的云原生功能。目前它提供了如下的扩展:
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容器化应用(Dockerfile
和Dockerfile.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
对于构建复杂的应用来讲,这是一个很好的起点,我们会在下一节讨论一个复杂的应用。
基于所生成的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 仓库以了解关于该应用的详细信息。
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.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