一个最简单的 Web 应用程序由 3 层构成,即客户端(前端)、服务器端(后端)和持久层(数据库)。全栈开发指的是针对 Web 应用程序的全部 3 层的实践。
MERN 栈- 图片来源
在上面提到的每一层使用的所有技术中,有一些技术比其他技术更受欢迎。当这些覆盖所有三层的技术结合在一起时,我们称之为“栈”,近些年来,我们已经看到了几个流行的栈。
在全栈开发方面,SPA(单页应用程序)和 PWA(渐进式 Web 应用程序)正在成为规范,并且出现了 SSR(服务器端渲染)等概念来解决它们的局限性。这些 Web 应用程序(前端)应该与后端 API(REST、GraphQL 等)一起工作,以便为终端用户提供最终功能。随之出现了诸如 BFF(服务于前端的后端)之类的概念,以使后端 API 与前端用户体验 (UX) 保持一致。
一个组织可以有多个微服务,这些服务被不同的使用方使用,如移动应用程序、Web 应用程序、其他服务/API 和外部使用方。然而,现代 Web 应用程序需要一个紧密耦合的 API 来与前端 UX 紧密配合, 因此,BFF 充当了前端和微服务之间的接口。
一个 BFF 调用多个下游服务在前端构造一个视图。下游 API 可以是不同的类型(REST、GraphQL gRPC 等)。阅读模式:服务于前端的后端来深入了解 BFF 架构模式。
记住上面的概念,让我们进一步讨论一下现代 Web 开发。
开发后端 API 可能意味着两件事:
如今,开发人员不会因为一个栈很流行就去使用它,他们选择最合适的前端技术来匹配他们希望实现的 UI/UX。然后他们选择后端技术时会考虑几个因素,包括其上市时间、可维护性和开发人员的经验。
在这篇文章中,我将介绍一个新的并且很有前景的后端开发候选技术,Ballerina。在将来,当你在为全栈开发做技术选型时,可以考虑一下它。
Ballerina 编程语言徽标
Ballerina 是一种开源的云原生编程语言,旨在简化网络服务的使用、组合和创建。Ballerina Swan Lake 是 Ballerina 语言于今年发布的下一个主要版本,它在所有方面都进行了重大改进,包括改进的类型系统、增强的类 SQL 语言集成查询、增强/直观的服务声明等等。
Ballerina 背后的主要动机是**让开发人员能够专注于业务逻辑,同时减少集成云原生技术所需的时间。**使用 Ballerina 的内置网络原语直接在云上定义和运行服务的方式在这场变革中发挥了关键作用。灵活的类型系统、面向数据的语言集成查询、增强和直观的并发处理以及内置的可观察性和跟踪支持,使 Ballerina 成为云时代的首选语言之一。
在开发前端直接调用的 API 时,我们有几个常用的选择:
Ballerina 编程语言的主要目标之一是简化网络交互代码的编写。考虑到这一点,Ballerina 在语言中内置了网络原语。当其他主流编程语言都将网络视为另一种 I/O 资源时,Ballerina 为网络交互提供了更为优秀的支持。为了实现这一目标,Ballerina 采用了以下优秀的组件设计:
Ballerina 特性一览图
让我们探索一下如何使用 Ballerina 对 REST 和 GraphQL API 的支持来编写直观且有意义的后端 API。请按照入门指南安装和设置 Ballerina。
设置 Ballerina
让我们看看如何使用 Ballerina 编写 REST API。
说 "Hello World!"
用 Ballerina 编写的 hello world REST API 如下所示:
import ballerina/http; service / on new http:Listener(8080) { resource function get greeting() returns string { return "Hello!"; }}
复制代码
让我们在这里解码语法的每个部分:
Ballerina HTTP 服务结构(来源)
要深入了解 Ballerina HTTP 服务语法,尤其是如何使用查询和路径参数、payload 数据绑定等,请参考以下文章:
HTTP Deep-Dive with Ballerina: Services
以下是一个稍微复杂一点的 REST API。给定基准货币、目标货币和金额,此 API 将返回转换后的金额。此 API 使用外部服务来获取最新汇率。
import ballerina/log;import ballerina/http; configurable int port = 8080; type ConversionResponse record { boolean success; string base; map<decimal> rates;}; service / on new http:Listener(port) { resource function get convert/[string baseCurrency]/to/[string targetCurrency](decimal amount) returns decimal|error { http:Client exchangeEP = check new ("https://api.exchangerate.host"); ConversionResponse response = check exchangeEP->get(string `/latest?base=${baseCurrency}`); if !response.success { return error("Exchange rates couldn't be obtained"); } decimal? rate = response.rates[targetCurrency]; if rate is () { return error("Couldn't determine exchange rate for target currency", targetCurrency = targetCurrency); } log:printInfo("converting currency", baseCurrency = baseCurrency, targetCurrency = targetCurrency, amount = amount); return rate * amount; }}
复制代码
与 hello world 示例相比,这个示例展示了 Ballerina 一些更有趣的功能。
curl http://localhost:8080/convert/USD/to/GBP?amount=100
复制代码
Ballerina 具有无泄漏的图形表示。也就是说,您可以同时编辑源代码和低代码视图(图形表示)。下图是上述 API 的低代码视图:
上述货币转换 API 的低代码视图
尽管我们不会在 Ballerina 的低代码方向做过多探索,但它对于非技术或技术水平较低的人来说,这有助于他们理解和编写代码,所以也试一试吧。
无泄漏 — 任何东西都可以用代码编程,代码中的一切都是可视的。
下面是一个用 Ballerina 编写的 CRUD 服务示例,它操作一组保存在内存中的产品。
import ballerina/http;import ballerina/log;import ballerina/uuid; # 表示一种产品public type Product record {| # Product ID string id?; # Name of the product string name; # Product description string description; # Product price Price price;|}; # 表示货币的枚举public enum Currency { USD, LKR, SGD, GBP} # 表示价格public type Price record {| # Currency Currency currency; # Amount decimal amount;|}; # 表示错误public type Error record {| # Error code string code; # Error message string message;|}; # 错误响应public type ErrorResponse record {| # Error Error 'error;|}; # 错误的请求响应public type ValidationError record {| *http:BadRequest; # Error response. ErrorResponse body;|}; # 表示已创建响应的标头public type LocationHeader record {| # Location header. A link to the created product. string location;|}; # 产品创建响应public type ProductCreated record {| *http:Created; # Location header representing a link to the created product. LocationHeader headers;|}; # 产品更新响应public type ProductUpdated record {| *http:Ok;|}; # 产品服务service / on new http:Listener(8080) { private map<Product> products = {}; # 列出所有产品 # + return - List of products resource function get products() returns Product[] { return self.products.toArray(); } # 添加一个新产品 # # + product - Product to be added # + return - product created response or validation error resource function post products(@http:Payload Product product) returns ProductCreated|ValidationError { if product.name.length() == 0 || product.description.length() == 0 { log:printWarn("Product name or description is not present", product = product); return <ValidationError>{ body: { 'error: { code: "INVALID_NAME", message: "Product name and description are required" } } }; } if product.price.amount < 0d { log:printWarn("Product price cannot be negative", product = product); return <ValidationError>{ body: { 'error: { code: "INVALID_PRICE", message: "Product price cannot be negative" } } }; } log:printDebug("Adding new product", product = product); product.id = uuid:createType1AsString(); self.products[<string>product.id] = product; log:printInfo("Added new product", product = product); string productUrl = string `/products/${<string>product.id}`; return <ProductCreated>{ headers: { location: productUrl } }; } # 更新一个产品 # # + product - Updated product # + return - A product updated response or an error if product is invalid resource function put product(@http:Payload Product product) returns ProductUpdated|ValidationError { if product.id is () || !self.products.hasKey(<string>product.id) { log:printWarn("Invalid product provided for update", product = product); return <ValidationError>{ body: { 'error: { code: "INVALID_PRODUCT", message: "Invalid product" } } }; } log:printInfo("Updating product", product = product); self.products[<string>product.id] = product; return <ProductUpdated>{}; } # 删除一个产品 # # + id - Product ID # + return - Deleted product or a validation error resource function delete products/[string id]() returns Product|ValidationError { if !self.products.hasKey(<string>id) { log:printWarn("Invalid product ID to be deleted", id = id); return { body: { 'error: { code: "INVALID_ID", message: "Invalud product id" } } }; } log:printDebug("Deleting product", id = id); Product removed = self.products.remove(id); log:printDebug("Deleted product", product = removed); return removed; }}
复制代码
大部分语法是不言自明的,该服务有 4 种资源方法:
# 错误的请求响应public type ValidationError record {| *http:BadRequest; # Error response. ErrorResponse body;|}; # 产品创建响应public type ProductCreated record {| *http:Created; # Location header representing a link to the created product. LocationHeader headers;|};
复制代码
拥有这样的模式有助于开发人员轻松理解代码。只需查看资源方法定义,开发人员就可以清楚地了解资源方法。什么是资源路径,需要什么查询/路径参数,有效负载是什么,以及可能的返回类型是什么。
resource function post products(@http:Payload Product product) returns ProductCreated|ValidationError { }
复制代码
这是一个 POST 请求,发送到 /products(通过查看资源方法派生),需要 Product 类型的有效负载,并返回验证错误 (400) 或带有位置标头的 HTTP CREATED 响应 (201)。
一旦我们用 Ballerina 编写了服务,只需指向源文件即可生成 OpenAPI 规范。通过查看源代码,它将输出带有相应状态代码和模式的 OpenAPI 规范。
您可以在 OpenAPI 部分阅读更多内容:
生成完整的 OpenAPI 规范可帮助您生成所需的客户端。在我们的例子中,生成 JavaScript 客户端并将我们的前端与后端轻松集成。
您可以通过将 HTTP 侦听器更新为 HTTPS 侦听器来保护您的服务,如下所示。
http:ListenerSecureSocket secureSocket = { key: { certFile: "../resource/path/to/public.crt", keyFile: "../resource/path/to/private.key" }};service /hello on new http:Listener(8080, secureSocket = secureSocket) { resource function get world() returns string { return "Hello World!"; }}
复制代码
您也可以启用双向 SSL 并进行高级配置。更多信息请参阅有关 HTTP 服务安全性的 Ballerina 示例。
Ballerina 内置了对 3 种身份验证机制的支持。
您可以提供证书文件或授权服务器的 JWK 端点 URL 并启用 JWT 签名验证。例如,如果我们要使用像 Asgardeo 这样的 IDaaS(身份即服务)来保护我们的服务,我们只需在服务中添加以下注解:
@http:ServiceConfig { auth: [ { jwtValidatorConfig: { signatureConfig: { jwksConfig: { url: "https://api.asgardeo.io/t/imeshaorg/oauth2/jwks" } } } } ]}
复制代码
此外,
与 JWT 类似,您可以使用 OAuth2 保护您的服务。有关更多详细信息,请参阅服务 - OAuth2 示例。
对于基本身份验证,有 2 个用户存储选项可用;文件和 LDAP。请参考以下示例以了解它是如何完成的:
使用 OAuth2 和 JWT,您可以验证每个服务或每个资源的作用域。在这两种情况下,您都可以指定自定义范围键,默认值为 scope。
对于 JWT,您可以使用包含用户角色(基于角色的访问控制 — RBAC)或权限(细粒度访问控制)的自定义声明来授权单个操作。
import ballerina/http; public type Product record {| string id?; string name; string description;|}; Product[] products = []; @http:ServiceConfig { auth: [ { jwtValidatorConfig: { issuer: "wso2", audience: "example.com", scopeKey: "permissions", signatureConfig: { jwksConfig: { url: "https://api.asgardeo.io/t/imeshaorg/oauth2/jwks" } } } } ]}service /products on new http:Listener(8080) { @http:ResourceConfig { auth: { scopes: "product:view" } } resource function get .() returns Product[] { return products; } @http:ResourceConfig { auth: { scopes: "product:create" } } resource function post .(@http:Payload Product product) returns error? { products.push(product); }}
复制代码
如上所示,/products 服务验证传入的 JWT 是否具有列出产品的 product:view 权限和创建产品的 product:create 权限。scopeKey 设置为 permissions,这是要在 JWT 中进行验证的声明的名称。此外,它还验证了发行者和受众。
显然,在编写后端 API 时,您必须与外部服务进行通信,至少你需要一个数据库客户端,无论是 DB 客户端、HTTP 客户端还是 gRPC 客户端,Ballerina 都能很好地满足您的需求。最重要的是,Ballerina 中的客户端调用是非阻塞的,开发人员无需添加任何回调或侦听器。
看看 Ballerina 的客户端有多方便:
为了使这篇文章简短,我不会深入讨论如何编写 GraphQL API,但是,与 REST API 类似,Ballerina 对 GraphQL 服务具有相同级别的支持。请参阅以下链接以了解更多信息:
我也不会深入讨论这个问题。有关编写 WebSocket 服务的更多详细信息,请参阅 Ballerina 网站的示例参考部分下的 WebSockets 和 WebSocket 安全部分。
可观察性是该语言内置的关键特性之一。使用 Ballerina,您可以开箱即用地执行分布式跟踪和指标监控。
分布式追踪可通过 Jaeger 和 Choreo 实现。为了将追踪发布到 Jaeger(或 Choreo),您只需在代码中添加一行导入。在运行时,您的服务将使用 Open Telemetry 标准将追踪发布到 Jaeger(或 Choreo)。
使用 Jaeger 和 Ballerina 进行分布式跟踪
Ballerina 提供的日志框架非常适合使用 logstash 和类似的日志分析器进行日志分析。在编写代码时,您可以将额外的键值对传递到日志行。
log:printInfo("This is an example log line", key1 = "key1", payload = { id: 123});
复制代码
上述日志行的输出如下所示:
time = 2022-01-26T16:19:38.662+05:30 level = INFO module = "" message = "This is an example log line" key1 = "key1" payload = {"id":123}
复制代码
可以使用 Prometheus 和 grafana 监控实时指标。此外,还可以使用 Choreo 监控实时指标..
Choreo 中的可观察性视图(来源)
与分布式跟踪类似,只需向源代码中添加一行导入,并导入一个现成的 grafana 仪表板,即可发布和监控实时指标。
Ballerina 的实时指标 Grafana 仪表板
从以下链接阅读有关 Ballerina 可观察性功能的更多信息:
后端开发的下一个主要方面是持久层。Ballerina 拥有丰富的 SQL 和 NoSQL DB 客户端。
通过客户端进行的 DB 调用在 Ballerina 中是非阻塞的
截至到目前,以下客户端是可用的:
mysqlClient->execute(`insert into products (product_name, price, currency) values (${product.productName}, ${product.price}, ${product.currency})`); mysqlClient->execute(`update products set product_name = ${product.productName}, price = ${product.price}, currency=${product.currency} where id=${product?.id}`);
复制代码
在上面的示例中,${<variableName>} 表示绑定到查询的变量,上面的代码在运行时作为准备好的语句执行。
类似地,我们可以使用如下的选择查询将数据作为用户定义类型的流来获取。假设我们有如下的产品记录:
type Product record {| int id?; string productName; float price; string currency;|};
复制代码
产品表定义如下:
CREATE TABLE `products` ( `id` int NOT NULL AUTO_INCREMENT, `product_name` varchar(255) NOT NULL, `price` float DEFAULT NULL, `currency` varchar(5) DEFAULT NULL, PRIMARY KEY (`id`))
复制代码
您可以将数据作为产品记录流获取,如下所示:
stream<Product, error?> productStream = mysqlClient->query(`select id, product_name as productName, price, currency from products`);
复制代码
请注意,记录字段名称和提取的列名称是相似的。
注意:Ballerina 的生态系统仍在完善过程中。因此,还没有功能齐全的 ORM 可用。
正如我们已经看到的,Ballerina 内置了对主流传输格式 JSON 和 XML 的支持。使用 Ballerina,您可以在用户定义的类型和 JSON 之间无缝转换,正如我们在 HTTP 服务和 DB 示例中看到的那样。
Ballerina 不使用名义类型(如 Java/Kotlin),而是依赖结构类型(如 Go/TypeScript)来确定子类型。这允许开发人员在用户定义的类型之间以及在用户定义的类型和 JSON 之间无缝转换。
Ballerina 是静态类型的。这使得 Ballerina 能够提供一组丰富的工具来编写可靠且可维护的代码。同时,Ballerina 遵循“默认开放”的原则,您只需定义您感兴趣的内容,开放记录就是这种用法的一个例子。
应显式处理错误。正如我们在示例中看到的,客户端调用返回结果和错误的并集。开发人员应该键入 check 检查错误,并显式地处理它们。
Ballerina 是空安全的。
结合以上所有方面,Ballerina 成为一种可维护且可靠的网络编程语言。
正如我们简要看到的,Ballerina 有一个非常好的、无泄漏的低代码特性。Ballerina 使用序列图来可视化网络交互,这对于技术水平较低和非技术人员编写程序和理解程序非常有用。
在本文中,我想简单而全面地概述一下 Ballerina 编程语言对编写后端 API 的支持。我们介绍了如何深入编写 REST API,我们还研究了如何保护服务、如何执行身份验证和授权以及生成 OpenAPI 规范。
接下来,我们简要介绍了如何编写 GraphQL 和 WebSocket 服务,然后我们研究了可观察性特性和持久性层支持(针对 SQL 和 NoSQL 数据库),最后,我们看了一些值得注意的 Ballerina 特性。
我认为所讨论的内容将有助于您更多地探索 Ballerina 编程语言,并了解它在后端 API 开发环境中的重要性。我希望我能够确定 Ballerina 是一种真正的云原生编程语言,它将网络原语视为一等公民。我邀请你进一步探索 Ballerina 编程语言。
感谢您关注本文,随时分享您的意见和建议。此外,如果您还有其他问题,请联系我或 Ballerina 社区。
作者介绍:
Imesha Sudasingha 是 WSO2 的高级软件工程师,他还是 Apache 软件基金会的成员,现任 Apache OODT 项目管理委员会主席,他一直是开源的持续贡献者和推动者,在多个领域拥有工作经验,包括企业集成、支付和 DevOps,他目前参与了 Ballerina 项目,通过 IDE 工具参与改善 Ballerina 用户的开发人员体验。
原文链接:
https://www.infoq.com/articles/ballerina-fullstack-rest-api
了解更多软件开发与相关领域知识,点击访问 InfoQ 官网:https://www.infoq.cn/,获取更多精彩内容!