@lsmn
2021-10-08T14:01:49.000000Z
字数 11788
阅读 878
202109
从2019年1.0版本发布以来,Ballerina 语言已经取得了长足的发展。最新的Swan Lake版本进一步简化了云原生应用的构建和部署。
集成可以看作是一种编程类型,而且为了简化和抽离集成的复杂性,人们借助不同的技术实现了集成的可视化表示。DSL已经变得非常流行,因为它们提供了恰当的编程抽象,但也有一些局限——很多时候,集成开发人员都不得不使用常规代码来解决一部分问题。而且,集成编程实践已经变成了孤岛,开发人员要选择一种集成工具进行集成编程,还必须使用另外一种工具或编程语言开发应用程序的其他部分。可视化表示还是很重要,我们可以借此观察端点之间的数据流和交互。此外,对于云原生工程,集成系统现在运行在容器中,应用程序使用分布在许多节点上的微服务来实现。
如果有一种语言既能提供代码集成能力,又能提供可视化工具,那岂不是非常有用?那样,我们岂不是既可以克服DSL的局限,又能够保留软件工程的最佳实践?Ballerina 就是在这种想法下诞生的。其目标是创建一种现代化的编程语言,一种集编程语言、集成技术和云原生计算优点于一体的、集成优化的、有潜力成为主流的文本和图形语言。
下面让我们逐条看下Ballerina 语言的关键特性。有些之前已经介绍过,有些在Swan Lake Beta版本中做了增强。从中我们可以理解,为什么Ballerina很适合创建网络服务及实现分布式系统集成。
你可以想象有这样一个语言图谱,从像Perl、Awk这样的脚本语言到像Rust、C这样的系统语言,中间是像Go、Java这样的应用程序语言。程序可以是单行脚本,也可以是数百万行代码。在这个图谱上,Ballerina 介于脚本语言和应用程序语言之间。
Ballerina 独有的特性使其非常适合小型程序。其他大多数为小型程序设计的脚本语言与Ballerina 有很大的不同,它们是动态类型的,而且无法提供Ballerina 独有的可扩展性和健壮性。在前云时代,你用其他脚本语言解决的问题仍然是很重要的问题。只是现在会涉及到网络服务;健壮性也比以往任何时候更重要。使用标准的脚本语言,一个50行的程序几年后往往就会变成一个上千行的、难以维护的程序,而这还不是终点。Ballerina 可以用于解决脚本程序的问题,而且更具扩展性,更健壮,也更适合云。此外,脚本语言通常也不提供任何可视化组件,但Ballerina 提供。
在网络交互中,面向对象的方法将数据和代码绑在一起,在分布广泛的微服务和API网络中,这并不是最佳的数据发送方式。
在前云时代,API是对路径中库函数的调用,你可以在调用中传递对象。但当API在云上时,就没法这样做了。你会希望通过网络发送的数据独立于代码,因为你不想暴露代码。虽然Java RMI和CORBA都曾设法支持分布式对象,但前提是紧耦合,而且两端同属一个组织。在松耦合的云上,分布式对象就无法使用了。Ballerina 突出的是纯数据,独立于任何处理数据的代码。虽然Ballerina 为内部接口提供了对象,但它不是一种面向对象的语言。
随着云上的服务越来越多,开发人员又多了在代码中处理网络资源的职责。编程语言本身必须提供相应的帮助。这就是为什么Ballerina 带来了一个网络友好的类型系统,提供了强大的在线数据处理功能。
JSON在Ballerina中是一种通用语言。Ballerina 中的数据类型非常接近JSON,数值、字符串、Map数组等基础数据类型可以一一映射到JSON。Ballerina的普通内存数据值几乎就是内存中的JSON。这样,通过网络传输过来的JSON负载可以立即由Ballerina处理,不需要转换或序列化。
import ballerina/io;
import ballerina/lang.value;
json j = { "x": 1, "y": 2 };
// 以JSON格式返回表示j的字符串。
string s = j.toJsonString();
// 解析JSON格式的字符串,返回它代表的值。
json j2 = check value:fromJsonString(s);
// 为了兼容JSON,允许null值。
json j3 = null;
public function main() {
io:println(s);
io:println(j2);
}
编程语言的类型系统是为了让你可以描述各部分是如何组合在一起的,而不仅仅是捕获一类错误——这只是类型系统为你做的一小部分工作。此外,它还有很大一部分工作是提供良好的IDE体验。
脚本语言是动态类型的,而应用程序语言则是传统的静态类型,像C++或Java。前文已经介绍过,Ballerina 是一种脚本语言,但它提供了一些应用程序语言的特性,其中就包括静态类型系统。在静态类型语言中,类型兼容性是在编译时检查的。通常,静态类型语言更便于重构、更容易调试,也有助于创建更好的语言工具。
虽然Ballerina的类型系统是静态的,但其类型比应用程序语言中的类型要灵活得多。Ballerina 语言的类型系统是结构性的,并且增加了对标明类型(nominal typing)的支持。也就是说,类型兼容性的认定考虑了值的结构,而不仅仅是依赖类型名称。这不同于Java、C++、C#等拥有标明类型系统的语言,它们的类型兼容性受实际的类型名称约束。这样一来,我们付出的代价是有些东西可能在编译时无法捕获,但收获了简洁性和灵活性。
具体来说,你可以说它类似于XML 模式中的结构定义方式。如果程序收到的XML负载有变化或偏差,它仍然可以处理能够识别的内容。它不是那么严格,负载中有无法识别的变化也不一定会失败。Ballerina的类型系统既可以作为描述网络数据的模式语言,也可以作为操作内存值的程序的类型系统。
要了解关于Ballerina类型系统的更多信息,请移步这里。
Ballerina 还提供了一套用于数据处理的语言特性,其中最突出的就是集成查询。该特性让你可以像下面这样使用类似SQL的语法查询数据。查询表达式通过一组类似SQL的子句来处理数据。它们必须以from子句开头,可以执行过滤、连接、排序、范围、投影等操作。
import ballerina/io;
type Employee record {
string firstName;
string lastName;
decimal salary;
};
public function main() {
Employee[] employees = [
{firstName: "Rachel", lastName: "Green", salary: 3000.00},
{firstName: "Monica", lastName: "Geller", salary: 4000.00},
{firstName: "Phoebe", lastName: "Buffay", salary: 2000.00},
{firstName: "Ross", lastName: "Geller", salary: 6000.00},
{firstName: "Chandler", lastName: "Bing", salary: 8000.00},
{firstName: "Joey", lastName: "Tribbiani", salary: 10000.00}
];
// 用于list解析的类SQL查询,以from开始,以select结束。
// order by子句根据姓氏对employees中的成员进行排序。
Employee[] sorted = from var e in employees
order by e.lastName ascending
select e;
io:println(sorted);
}
Ballerina 还提供了一个Table 数据类型,简化关系型表格数据的处理。下面的代码创建了一个表,其中是Employee类型的成员,每位成员使用name字段唯一标识。main函数通过键值John 检索Employee,并增加每名员工的工资。
import ballerina/io;
type Employee record {
readonly string name;
int salary;
};
// 创建一个表,其成员类型为Employee,其中每位成员使用name字段唯一标识。
table<Employee> key(name) t = table [
{ name: "John", salary: 100 },
{ name: "Jane", salary: 200 }
];
function increaseSalary(int n) {
// 按指定的顺序遍历t中的行。
foreach Employee e in t {
e.salary += n;
}
}
public function main() {
// 使用键值`John`检索Employee。
Employee? e = t["John"];
io:println(e);
increaseSalary(100);
io:println(t);
}
要想了解更多关于Ballerina 集成查询的信息,请移步这里。
此外,Ballerina内置了XML支持,其功能类似于XQuery,具有类似XPath的XML导航机制。这特别适合那些大量使用XML但又不想使用XML专用语言的人,因为他们现如今要处理各种数据格式。
import ballerina/io;
public function main() returns error? {
xml x1 = xml `<name>Sherlock Holmes</name>`;
xml:Element x2 =
xml `<details>
<author>Sir Arthur Conan Doyle</author>
<language>English</language>
</details>`;
// `+`做串联。
xml x3 = x1 + x2;
io:println(x3);
xml x4 = xml `<name>Sherlock Holmes</name><details>
<author>Sir Arthur Conan Doyle</author>
<language>English</language>
</details>`;
// `==`做深层相等比较。
boolean eq = x3 == x4;
io:println(eq);
// `foreach`迭代每个数据项。
foreach var item in x4 {
io:println(item);
}
// `x[i]` 获取第i项数据(如果没有,则为空序列)。
io:println(x3[0]);
// `x.id`访问名为`id`的属性,如果属性不存在,或者不是一个单元素项,则返回错误。
xml x5 = xml `<para id="greeting">Hello</para>`;
string id = check x5.id;
io:println(id);
// `x?.id`访问名为`id`的可选属性,如果属性不存在,则结果为`()`。
string? name = check x5?.name;
io:println(name is ());
// 使用`e.setChildren(x)`转换元素。
x2.setChildren(xml `<language>French</language>`);
io:println(x2);
io:println(x3);
}
在无缝处理不同数据类型的能力中,Ballerina还提供了一个decimal 数据类型。这是为满足商业需求而设计的浮点数,如标注价格。由于在一般的语言中,值都是用二进制表示的,所以并不能准确地表示所有实数。当位数超出了格式限制时,剩余部分会被忽略——数值成了近似值,这会导致精度错误。真实世界是运转在十进制数上的,这也是为什么我们会认为这是Ballerina的一个强大能力。
import ballerina/io;
// `decimal`类型表示128位IEEE 754R十进制浮点数集合。
decimal nanos = 1d/1000000000d;
function floatSurprise() {
float f = 100.10 - 0.01;
io:println(f);
}
public function main() {
floatSurprise();
io:println(nanos);
}
在云上,并发是一个基本需求,因为网络操作有高延迟。脚本语言对于并发的处理通常也不是很好。典型地,像JavaScript这样的脚本语言使用异步函数,这比会回调稍微好点,但也好不了多少。Ballerina提供了一个更简单的编程模型,它提供的并发方法有异步函数的优点,但比异步函数更简单、直观。
在Ballerina中,主要的并发概念是strand,类似于Go语言中的goroutine。Ballerina 程序运行在一个或多个线程上。线程可以与其他线程一起同时运行在一颗内核上,也可以与其他线程一起在一颗内核上执行抢占式多任务。每个线程都可以分成一个或多个strand,这是由语言托管的逻辑控制线程。从程序员的角度来看,strand 像一个OS线程,但它不是——它更轻量级,开销更小。Strand可以调度到单独的OS线程上。Go采用了类似的方法,但这样做的编程语言并不多。事实上,大部分动态脚本语言都不支持并发。例如,Python有一个全局锁,所以它并不是真正支持并发执行。
Ballerina 中的函数可以拥有命名“worker”,每个worker并发运行在一个新的strand上,函数的默认worker和其他命名worker如下所示:
import ballerina/io;
public function main() {
// 任何命名worker之前的代码都会在worker启动之前执行。
io:println("Initializing");
final string greeting = "Hello";
// 命名worker和函数的默认worker以及其他命名worker并行运行。
worker A {
// 在所有命名worker和函数参数之前声明的变量都可以在命名worker中访问。
io:println(greeting + " from worker A");
}
worker B {
io:println(greeting + " from worker B");
}
io:println(greeting + " from function worker");
}
Ballerina 还允许strand之间共享不可变状态。通常,将并发和不可变状态共享相结合会导致数据竞争,产生错误结果。这是动态语言通常不暴露线程的其中一个原因。不过,对于这个问题,Ballerina 提供了一个很好的解决方案,通过协作式多任务保证并发安全性。在Ballerina 中,同一线程上的所有strand都是以协作式多任务(而非抢占式)方式执行,从而避免了锁问题。这类似于异步函数,所有东西在一个线程上运行,但没有复杂的编程模型。Strand 通过暂停来实现协作式多任务。当一个strand 在特定的”暂停点“暂停,运行时调度器就会挂起该strand的执行,将其线程切换为运行另一个strand。
我们还可以决定什么时候并行运行strand——可以使用注解让一个strand在一个单独的线程上运行。这是因为Ballerina 独特的类型系统使它能够确定服务何时已经锁定,从而可以安全地使用多线程并行处理传入的请求。虽然这看起来可能无法提供大量的并行执行,但也足以有效利用常见的云实例类型了。
import ballerina/io;
public function main() {
// 每个命名worker都有一个“strand”(逻辑线程控制),strand只在特定的“暂停点”切换执行。
worker A {
io:println("In worker A");
}
// 注解可以用于使一个strand在单独的线程上运行。
@strand {
thread: "any"
}
worker B {
io:println("In worker B");
}
io:println("In function worker");
}
处理并发和网络交互是编写云程序固有的一部分工作。在Ballerina 中,每个程序都可以自动生成一个序列图,通过图形说明分布式并发交互。Ballerina 程序中有等价的文本语法和序列图表示。你可以在这两种视图之间无缝切换。Ballerina 独有的图形化视图不是事后才有的想法。事实上,Ballerina 在设计时就做了深入的考虑,为的是在函数的网络交互和并发使用方面提供真正的洞察力。序列图是最适合这种情形的一种图形。
说明一下,Ballerina 的命名worker(第5点讨论过)以及其他函数级并发特性描述了并发性,而客户端和服务的语言抽象则描述了网络交互。竖线,也称为生命线,表示worker和远程端点。一个远程端点是一个客户端对象,它包含远程方法,表示和远程系统的出站交互。大部分语言都不区分远程调用和常规方法调用,但Ballerina 提供了截然不同的远程方法调用。横线表示从一个函数的worker发送给另一个worker或是远程端点的消息。这些方面在Ballerina 中很容易区分,它提供了高级视图,用户什么都不需要做。Ballerina 之所以能做到这种程度,是因为它从设计之初就加入了图形化元素。这是其他任何语言都没有的特性。
Ballerina VSCode插件可以从源代码动态生成序列图。要生成上述Ballerina 代码的序列图,请下载VSCode插件并启动图形查看器。
除了具有网络感知特点的类型系统外,Ballerina还提供了用于处理网络服务的基本语法抽象。该语言还内置支持使用Docker和Kubernetes在云上部署Ballerina应用程序。
生成服务的服务对象
Ballerina 迎合了服务的概念,使用Ballerina 只需3、4行代码就可以写出一个服务。在Ballerina 中,服务基于3个概念:应用程序、监听器和库。应用程序定义服务对象,并将它们连接到监听器。监听器由库提供。举例来说,每种协议(HTTP、GraphQL等)都有一个监听器,都由一个库提供。监听器接收网络输入,然后调用应用程序找到服务对象。服务对象支持以下两种接口类型:
得益于Ballerina提供的服务方法以及独特的面向连接的类型系统,你可以从Ballerina 代码生成一个接口描述,可以是OpenAPI 规格的,也可以是GraphQL 规格的。这样,你就可以实际编写服务对象,并生成客户端代码了。这些特性组合在一起就使得云集成可以顺利进行了。
import ballerina/http;
service on new http:Listener(9090) {
resource function get greeting(string name) returns string {
return "Hello, " + name;
}
}
消费远程服务的客户端对象
出站网络交互表示为客户端对象。客户端中有远程方法,表示和远程系统的出站交互。客户端对象是其中一个语法元素,让我们可以绘制序列图。
import ballerina/email;
function main() returns error? {
email:SmtpClient sc
= check new("smtp.example.com",
"user123@example.com",
"passwd123");
check sc -> sendMessage({
to: "contact@ballerina.io",
subject: "Ballerina"
body: "Ballerina is pretty awesome!"
});
}
代码上云
Ballerina 支持从代码生成Docker 和Kubernetes 工件,不需要任何额外的配置。这简化了开发以及向云上部署Ballerina 代码的体验。代码上云要先从代码中获取所需的值然后再构建容器和所需的工件。要了解详细信息,可以看下这个例子。要将代码部署到不同的云平台上,如AWS和微软Azure,可以使用服务对象注解轻松实现云部署,如下所示。Ballerina 编译器可以生成Dockerfile、Docker镜像、Kubernetes YAML文件、无服务器函数等工件。例如,Ballerina 函数可以使用@azure_functions:Function注解部署到Azure上。
import ballerina/uuid;
import ballerinax/azure_functions as af;
// 没有身份验证的HTTP请求/响应。
@af:Function
public function hello(@af:HTTPTrigger { authLevel: "anonymous" } string payload) returns @af:HTTPOutput string|error {
return "Hello, " + payload + "!";
}
错误处理方法对于语言设计和使用有着深远的影响。它会影响语言的方方面面。当你和网络打交道时,错误是正常业务处理的一部分,尤其是考虑到分布式计算的8大谬误时。像Java、JavaScript、TypeScript等前云时代的语言,使用异常作为其错误处理方式。但并不是每种语言都遵循那种设计。像Go和Rust这样的语言根本就没有异常。
使用异常,控制流是隐式的,代码理解和维护的难度都比较大。当出现问题时,只是方便地抛出一个异常,就会使什么东西都失控。为了实现恰当的错误处理,你必须得仔细看下程序,弄清楚可能出现错误的地方是否有错,以及控制流如何变化。所以,现在有一个相当强的趋势,就是消除异常,回归一种更简单的方法,错误是显式的,使用正常控制流进行处理。Go、Rust、Swift都使用了这种方法。Ballerina 也使用了这一方法,让开发人员可以使用error 数据类型,以及显式的错误控制流。
import ballerina/io;
// 将bytes转换成string,然后再转换成int。
function intFromBytes(byte[] bytes) returns int|error {
string|error ret = string:fromBytes(bytes);
// is操作符可以用于区分错误和其他值。
if ret is error {
return ret;
} else {
return int:fromString(ret);
}
}
// main会返回一个error。
public function main() returns error? {
int|error res = intFromBytes([104, 101, 108, 108, 111]);
if res is error {
// 可以使用`check`表达式来简化这个判断模式。
return res;
} else {
io:println("result: ", res);
}
}
编写使用事务的Ballerina 程序非常简单,因为事务是它的一个语言特性。Ballerina 提供的不是事务性内存,而是从根本上支持事务划分。这样就可以保证,事务总是包含begin、rollback或commit选项。
Ballerina 程序正在运行的实例中包含一个事务管理器。它可能是和Ballerina 程序在同一个进程中运行,也可能是在一个单独的进程中(连接网络要可靠)。事务管理器维护了从每个strand 到事务栈(或者是分布式上下文中的事务分支)的映射。当strand的事务栈非空时,我们就说它处于事务模式;strand事务栈最顶端的事务就是该strand当前的事务。
import ballerina/io;
public function main() returns error? {
// 编译时会保证事务以begin开始,以commit或rollback结束。Transaction 语句
// 会开启一个新事务,并执行一个代码块。
transaction {
doStage1();
doStage2();
// 事务要使用commit语句显式提交,这可能会导致一个错误。
check commit;
}
}
function doStage1() {
io:println("Stage1 completed");
}
function doStage2() {
io:println("Stage2 completed");
}
为了支持分布式事务,Ballerina 中的事务还加入了网络交互特性(即客户端和服务)。用户可以将服务的资源/远程方法以及客户端对象的远程方法声明为事务性的,从而创建客户端和服务之间的事务流。
新语言大量出现表明人们还是很愿意学习新语言。但是,企业似乎不太愿意采用新语言,因为他们会担心招聘不到熟悉那门语言的人。因此,要重点说明一下,Ballerina 不仅提供了更好的做事方式,而且还提供了一组C语言家族程序员熟悉的特性,他们只需几个小时甚至更少的时间就足以上手开发。
广为流行的C语言家族(C、C++、Java、JavaScript、C#、TypeScript)有许多共同点。Ballerina 利用了这一点,许多事情都采用了同样的方式。如果对于C语言家族中的任何一门语言,你有一定量的编程经验,那么使用Ballerina 编码都会相当简单。除了功能强大的语言特性外,Ballerina 还是“batteries included”的,即提供了丰富的标准库(包括用于网络数据、消息和通信协议的库)、包管理系统、结构化文档、测试框架,它还为流行的IDE(特别是Visual Studio Code)提供了扩展/插件,以便使该工具支持这门语言。
虽然Ballerina 具备现代编程语言的所有通用功能,但它的优势在于其提供了一些独特的语言特性,让开发人员可以更容易使用、组合和创建云端网络服务。开发人员现在可以构建富有弹性的、安全的、高性能的服务,消除分布式计算的谬误,并使用一种专门的编程语言将它们整合在一起创建云原生应用程序。
要想快速了解在Ballerina中如何创建以及消费HTTP服务,可以观看这个录屏视频。如果你更喜欢通过示例进行学习,那么这里提供了许多代码示例,从中你可以了解到Ballerina 的重要特性和概念。
要想深入了解Ballerina Swan Lake的语言特性,可以观看Ballerina首席语言设计师James Clark提供的系列视频。你还可以阅读这篇博文,了解Ballerina的设计原则。
Dakshitha Ratnayake目前在WSO2担任Ballerina的项目经理。她拥有软件工程的背景,在WSO2担任软件工程师、解决方案架构师和技术布道师等职务,拥有超过10年的经验。在此期间,她一直是WSO2 API管理、企业应用集成、身份和访问管理、微服务架构、事件驱动架构和云原生编程等领域的技术倡导者。与此同时,她还与不同的组织保持着技术关系,以理解业务需求,沟通技术战略。
查看英文原文:Ballerina Swan Lake: 10 Compelling Language Characteristics for Cloud Native Programming