Ballerina 面向全栈开发人员:创建后端 API 的指南

背景

一个最简单的 Web 应用程序由 3 层构成,即客户端(前端)、服务器端(后端)和持久层(数据库)。全栈开发指的是针对 Web 应用程序的全部 3 层的实践。

  • 前端开发涉及 HTML、CSS 和原生 JavaScript, 或者一到多个 JavaScript 框架/库(如 JQuery 或 React.js)。在过去几年中,可用的工具、框架和库呈指数级增长,因此,前端开发本身就是一个非常广泛的课题。
  • 后端开发通常涉及服务器端脚本语言(如 PHP 或 JSP)又或者前端使用的服务。随着单页Web 应用程序 (SPA) 的出现,开发人员已经摆脱了传统的服务器端脚本语言,转而采用 API(REST、GraphQL、WebSocket 等)作为后端。
  • 持久层包括一个或多个 SQL 或者 NoSQL 数据库。但随着“大数据”的出现,数据存储不再局限于传统的数据存储。
  • 热门的栈

    在上面提到的每一层使用的所有技术中,有一些技术比其他技术更受欢迎。当这些覆盖所有三层的技术结合在一起时,我们称之为“栈”,近些年来,我们已经看到了几个流行的栈。

  • LAMP / LEMP?—?JavaScript 用于前端。Linux 用于托管服务,Apache/Nginx 作为 Web 服务器,MySQL 作为持久层,PHP 作为后端(或服务器端)语言。像 Laravel 和 Symphony 这样的框架在这个栈下很流行。
  • MEAN?—?MongoDB 用于数据持久层,Express 作为后端框架,AngularJS 作为前端 JavaScript 框架,NodeJS 作为服务器运行时。
  • MERN?—?与 MEAN 栈相同,只是选择了 ReactJS 而不是 Angular。
  • Ruby on Rails?—?用 Ruby 编写的 Web 应用程序框架。
  • Django?—?JavaScript 用于前端,Python Django 框架用于后端,以及相应的 SQL 或 NoSQL 数据库层。然而,栈不再那么简单了。对于给定的一层,可用的替代技术成倍增加,在给定的层中,我们可以用来组合开发最终产品的不同技术的数量也在成倍增加。
  • 现代全栈开发

    在全栈开发方面,SPA(单页应用程序)和 PWA(渐进式 Web 应用程序)正在成为规范,并且出现了 SSR(服务器端渲染)等概念来解决它们的局限性。这些 Web 应用程序(前端)应该与后端 API(REST、GraphQL 等)一起工作,以便为终端用户提供最终功能。随之出现了诸如 BFF(服务于前端的后端)之类的概念,以使后端 API 与前端用户体验 (UX) 保持一致。

    BFF?—?服务于前端的后端

    一个组织可以有多个微服务,这些服务被不同的使用方使用,如移动应用程序、Web 应用程序、其他服务/API 和外部使用方。然而,现代 Web 应用程序需要一个紧密耦合的 API 来与前端 UX 紧密配合, 因此,BFF 充当了前端和微服务之间的接口。

    一个 BFF 调用多个下游服务在前端构造一个视图。下游 API 可以是不同的类型(REST、GraphQL gRPC 等)。阅读模式:服务于前端的后端来深入了解 BFF 架构模式。

    记住上面的概念,让我们进一步讨论一下现代 Web 开发。

    全栈开发背景下的后端开发

    开发后端 API 可能意味着两件事:

    1. 开发 BFF?—?充当前端 UX 和后端微服务之间的适配器。
    2. 开发微服务?—?开发前端直接或间接(通过 BFF)使用的单个微服务。在全栈开发的背景下,我们只需考虑由前端直接调用的后端 API。这些 API 可以编写为 BFF API 或单独的微服务。

    选择栈

    如今,开发人员不会因为一个栈很流行就去使用它,他们选择最合适的前端技术来匹配他们希望实现的 UI/UX。然后他们选择后端技术时会考虑几个因素,包括其上市时间、可维护性和开发人员的经验。

    在这篇文章中,我将介绍一个新的并且很有前景的后端开发候选技术,Ballerina。在将来,当你在为全栈开发做技术选型时,可以考虑一下它。

    什么是 Ballerina?

    Ballerina 编程语言徽标

    Ballerina 是一种开源的云原生编程语言,旨在简化 络服务的使用、组合和创建。Ballerina Swan Lake 是 Ballerina 语言于今年发布的下一个主要版本,它在所有方面都进行了重大改进,包括改进的类型系统、增强的类 SQL 语言集成查询、增强/直观的服务声明等等。

    Ballerina 背后的主要动机是**让开发人员能够专注于业务逻辑,同时减少集成云原生技术所需的时间。**使用 Ballerina 的内置 络原语直接在云上定义和运行服务的方式在这场变革中发挥了关键作用。灵活的类型系统、面向数据的语言集成查询、增强和直观的并发处理以及内置的可观察性和跟踪支持,使 Ballerina 成为云时代的首选语言之一。

    在开发前端直接调用的 API 时,我们有几个常用的选择:

  • REST API
  • GraphQL API
  • WebSocket API 一旦选择了合适的 API 类型,接下来我们必须考虑以下因素:
  • 安全通信
  • 验证
  • 授权
  • 监控、可观测性和日志记录除了上述因素外,我们在开发 API 时还必须考虑可维护性和开发者体验。让我们看看 Ballerina 如何为开发上述 API 类型提供支持,以及是什么让 Ballerina 成为后端 API 开发领域中一个有前景的候选者。
  • Ballerina 与 络交互

    Ballerina 编程语言的主要目标之一是简化 络交互代码的编写。考虑到这一点,Ballerina 在语言中内置了 络原语。当其他主流编程语言都将 络视为另一种 I/O 资源时,Ballerina 为 络交互提供了更为优秀的支持。为了实现这一目标,Ballerina 采用了以下优秀的组件设计:

  • 侦听器?—?充当 络层和服务层之间的接口。侦听器代表底层传输,如 HTTP、gRPC、TCP 或 WebSocket。
  • 服务—表示向最终用户公开组织功能的服务。HTTP、GraphQL、gRPC 和 WebSocket 是此类服务的一些例子。
  • 资源方法—?表示服务中的一个功能单元。例如,如果我们考虑使用一个简单的 CRUD 服务来管理库存,添加库存由一个单独的资源方法表示,而删除库存操作则由另一个资源方法表示。
  • 客户端—如今编写服务通常包括调用一个或多个外部或内部服务。例如:
    1. 在你的一项服务中,你可能想要发送一封电子邮件。为此,你需要一个电子邮件客户端。2. 同一个服务可能需要调用一个或多个内部 gRPC 服务。为此,你需要 gRPC 客户端。同样,编写服务需要调用外部服务。为此,Ballerina 有一个丰富的概念,称为客户端,外部调用由远程方法表示。在 Ballerina 运行时中调用远程方法是异步的(非阻塞,同时不需要显式回调或侦听器)。这些语言内置的 络原语与其他语言特性(如显式错误处理、内置 json/xml 支持和灵活的类型)相结合,可帮助开发人员更快地编写直观且可维护的 络交互,这反过来又使开发人员和组织能够更多地关注创新。

    Ballerina 特性一览图

    让我们探索一下如何使用 Ballerina 对 REST 和 GraphQL API 的支持来编写直观且有意义的后端 API。请按照入门指南安装和设置 Ballerina。

    设置 Ballerina

    开发 REST API

    让我们看看如何使用 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!";   }}

    复制代码

    让我们在这里解码语法的每个部分:

  • import ballerina/http;– 导入 ballerina/http 包。
  • service / on new http:Listener(8080)– 在 HTTP 侦听器上创建一个上下文路径为“/”的服务,该侦听器侦听 8080 端口,服务的类型由附加的侦听器的类型决定。在当前实例中,由于我们的侦听器是 HTTP 类型的,所以它就是一个 HTTP 服务。
  • resource function get greeting() returns string– 表示可以通过此 HTTP 服务执行单个操作。“get”是“资源访问器”。简单来说,它代表 HTTP 方法(get、post、delete 等)。“greeting”是函数名,函数名作为路径,这意味着资源路径“/greeting”由该资源方法处理。“returns string”表示此服务返回一个字符串,我们也可以在这里返回复杂的对象。
  • return “Hello World!”;– 表示资源方法的返回值。这里是字符串“Hello World”。下图显示了 Ballerina HTTP 服务语法的概述:
  • 要深入了解 Ballerina HTTP 服务语法,尤其是如何使用查询和路径参数、payload 数据绑定等,请参考以下文章:

    HTTP Deep-Dive with Ballerina: Services

    集成示例?—?货币转换 API

    以下是一个稍微复杂一点的 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 一些更有趣的功能。

  • service / on new http:Listener(port) – 基础路径现在是 /,端口是可配置的,这意味着它可以在运行时进行配置,正如 configurable int port = 8080 中所定义的,端口的默认值为 8080,并且是可配置的,可配置变量也是一个值得注意的特性。
  • resource function get convert/[string baseCurrency]/to/[string targetCurrency](decimal amount) returns decimal|error – 在这种情况下,资源路径是 /convert/{baseCurrency}/to/{targetCurrency} 并且现在需要一个名为 amount 的查询参数。此资源方法会返回一个十进制值(转换速率)或一个错误(映射到 500 – 内部服务器错误)。
  • ConversionResponse response = check dccClient->get(string /latest?base=${baseCurrency}) – 调用外部汇率 API,并将响应转换为公开的记录 ConversionResponse。此调用是非阻塞的。响应有效载荷被无缝地转换为了一个公开的记录,这体现了 Ballerina 的默认开放原则。运行后,如下所示的 curl 请求会将 100 美元转换为英镑。
  • curl http://localhost:8080/convert/USD/to/GBP?amount=100

    复制代码

    红利:低代码开发

    上述货币转换 API 的低代码视图

    尽管我们不会在 Ballerina 的低代码方向做过多探索,但它对于非技术或技术水平较低的人来说,这有助于他们理解和编写代码,所以也试一试吧。

    无泄漏?—?任何东西都可以用代码编程,代码中的一切都是可视的。

    一个简单的 CRUD 服务

    下面是一个用 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 种资源方法:

  • 列出所有产品?—?GET /products
  • 添加新产品?—?POST /products
  • 更新产品?—?PUT /product
  • 删除产品?—?DELETE /products/{id}请注意如何定义类型来表示产品、价格和货币。然后,我们在需要实现所需模式的地方定义了响应类型,ProductCreated 表示添加产品的响应,ValidationError 表示验证中的错误。
  • # 错误的请求响应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)。

    生成 OpenAPI 规范

    一旦我们用 Ballerina 编写了服务,只需指向源文件即可生成 OpenAPI 规范。通过查看源代码,它将输出带有相应状态代码和模式的 OpenAPI 规范。

    您可以在 OpenAPI 部分阅读更多内容:

    Ballerina 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 种身份验证机制的支持。

    JWT

    您可以提供证书文件或授权服务器的 JWK 端点 URL 并启用 JWT 签名验证。例如,如果我们要使用像 Asgardeo 这样的 IDaaS(身份即服务)来保护我们的服务,我们只需在服务中添加以下注解:

    @http:ServiceConfig {   auth: [       {           jwtValidatorConfig: {               signatureConfig: {                   jwksConfig: {                       url: "https://api.asgardeo.io/t/imeshaorg/oauth2/jwks"                   }               }
    
                                                            

    声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

    上一篇 2022年3月24日
    下一篇 2022年3月24日

    相关推荐