我是前 Sun 公司 Java SE 团队的一名成员,在工作了 10 多年之后——2009 年 1 月——也就是在甲骨文收购 Sun 公司之前,我离开了公司,然后迷上了 Node.js。
我对 Node.js 的痴迷到了怎样的程度?自 2010 年以来,我撰写了大量有关 Node.js 编程的文章,出版了四本与 Node.js 开发有关的书籍,以及与 Node.js 编程有关的其他书籍和众多教程。
在 Sun 公司工作期间,我相信 Java 就是一切。我在 JavaONE 上发表演讲,共同开发了 java.awt.Robot 类,组织 Mustang 回归竞赛(Java 1.6 版本的漏洞发现竞赛),协助推出了“Java 发行许可”,这在后来的 OpenJDK 项目启动过程中起到了一定的作用。我在 java.net(这个网站现已解散)上每周写一到两篇博文,讨论 Java 生态系统中所发生的主要事件,并坚持了 6 年。这些博文的主要主题是关于“保卫”Java,因为总有人在预言 Java 的“死期”。
在这篇文章中,我将会解释我这个 Java 死忠是如何变成一个 Node.js 和 JavaScript 死忠的。
但其实我并没有完全脱离 Java。在过去的三年中,我编写了大量 Java/Spring/Hibernate 代码。但两年的 Spring 编码经历让我明白了一个道理:隐藏复杂性并不会带来简单性,它只会产生更多的复杂性。
Java 是负担,Node.js 充满了乐趣
有些工具是设计师花费数年磨砺和精炼的结果。他们尝试不同的想法,去掉不必要的属性,最终得到一个只带有恰到好处属性的工具。这些工具的简洁性甚至达到让人感到惊艳的程度,但 Java 显然不属于这一类。
Spring 是一个非常流行的用于开发 Java Web 应用程序的框架。Spring(特别是 Spring Boot)的核心目的是成为一个易于使用的预配置的 Java EE 栈。Spring 程序员不需要直接接触 Servlet、数据持久化、应用程序服务器就可以获得一个完整的系统。Spring 框架负责处理所有这些细节,你只需要把精力放在业务编码上。例如,JPA Repository 类为“findUserByFirstName”方法合成数据库查询——你不需要编写任何查询代码,只需按照特定方式给方法命名,并添加到 Repository 中即可,Spring 将负责处理其余的部分。
这原本是一个伟大的故事,一种很好的体验,但其实并不然。
当你遇到 Hibernate 的 PersistentObjectException 时,你知道是哪里出了问题吗?你可能花了几天时间才找到问题所在,导致这个异常的原因是发给 REST 端点的 JSON 消息里带有 ID 字段。Hibernate 想要自己控制 ID 值,所以抛出了这个令人感到困惑的异常。看,这就是过度简化所带来的恶果。除了这个,还有其他成千上万个同样令人感到困惑的异常。在 Spring 栈中,一个子系统套着另一个子系统,它们坐等你犯错,然后再抛出应用程序崩溃异常来惩罚你。
然后,你会看到满屏的堆栈跟踪信息,里面满是这样那样的抽象方法。面对这种级别的抽象,显然需要更多的逻辑才能找到你想要的内容。如此多的堆栈跟踪信息不一定是不好的,但它也是在提醒我们:这在内存和性能方面的开销究竟有多大?
而零代码的“findUserByFirstName”方法又是如何被执行的?Spring 框架必须解析方法名称,猜测程序员的意图,构造类似抽象语法树的东西,生成一些 SQL 语句……那么完成这个过程需要多少开销?
在反反复复经历这样的过程之后,在花了大量时间学习你本不该学习的东西之后,你可能会得出相同的结论:隐藏复杂性并不会带来简单性,它只会产生更多的复杂性。
Node.js 是清流
Spring 和 Java EE 非常复杂,而 Node.js 却是一股清流。首先是 Ryan Dahl 在核心 Node.js 平台上所应用的设计美学。他追求别样的东西,花了数年时间磨练和改进了一系列核心的 Node.js 设计理想,最终得到一个轻量级的单线程系统。它巧妙地利用了 JavaScript 匿名函数进行异步回调,成为一个实现了异步机制的运行时库。
然后是 JavaScript 语言本身。JavaScript 程序员似乎更喜欢无样板的代码,这样他们的意图才能发挥作用。
我们可以通过实现监听器的例子来说明 Java 和 JavaScript 之间的差别。在 Java 中,监听器需要实现抽象接口,还需要指定很多啰七八嗦的细节。程序员的意图的这些繁琐的样板中渐渐淹没。
而在 JavaScript 中,可以使用最简单的匿名函数——闭包。你不需要实现什么抽象接口,只需要编写所需的代码,没有多余的样板。
大多数编程语言都试图掩盖程序员的意图,这让理解代码变得更加困难。
但在 Node.js 中有一点需要注意:回调地狱。
没有完美的解决方案
在 JavaScript 中,我们一直难以解决两个与异步相关的问题。一个是 Node.js 中被称为“回调地狱”的东西。我们很容易就掉入深层嵌套回调函数的陷阱,每个嵌套都会使代码复杂化,让错误和结果的处理变得更加困难。但 JavaScript 语言并没有为程序员提供正确表达异步执行的方式。
于是,出现了一些第三方库,它们承诺可以简化异步执行。这是另一个通过隐藏复杂性带来更多复杂性的例子。
const async = require(‘async’); const fs = require(‘fs’); const cat =
function(filez, fini) { async.eachSeries(filez, function(filenm,
next) { fs.readFile(filenm, ‘utf8’, function(err, data) { if (err)
return next(err); process.stdout.write(data, ‘utf8’, function(err) {
if (err) next(err); else next(); }); }); }, function(err) { if
(err) fini(err); else fini(); }); }; cat(process.argv.slice(2),
function(err) { if (err) console.error(err.stack); });
这是个模仿 Unix cat 命令的例子。async 库非常适合用于简化异步执行顺序,但同时也引入了一堆模板代码,从而模糊了程序员的意图。
这里实际上包含了一个循环,只是没有使用循环语句和自然的循环结构。此外,错误和结果的处理逻辑被放在了回调函数内。在 Node.js 采用 ES 2015 和 ES 2016 之前,我们只能做到这些。
Node.js 10.x 中,等价的代码是这样的:
const fs = require(‘fs’).promises; async function cat(filenmz) { for
(var filenm of filenmz) { let data = await fs.readFile(filenm,
‘utf8’); await new Promise((resolve, reject) => {
process.stdout.write(data, ‘utf8’, (err) => { if (err) reject(err);
else resolve(); }); }); } } cat(process.argv.slice(2)).catch(err =>
{ console.error(err.stack); });
这段代码使用 async/await 函数重写了之前的逻辑。虽然异步逻辑是一样的,但这次使用了普通的循环结构。错误和结果的处理也显得很自然。这样的代码更容易阅读,也更容易编码,程序员的意图也更容易被理解。
唯一的瑕疵是 process.stdout.write 没有提供 Promise 接口,因此用在异步函数中时需要丢 Promise 进行包装。
回调地狱问题并不是通过隐藏复杂性才得以解决的。相反,是语言和范式的演变解决了这个问题。通过使用 async 函数,我们的代码变得更加美观。
通过明确定义的类型和接口提升清晰度
当我还是 Java 的死忠时,我坚信严格的类型检查对开发大型的应用程序来说是有百利而无一害的。那个时候,微服务的概念还没有出现,也没有 Docker,人们开发的都是单体应用。因为 Java 具有严格的类型检查,所以 Java 编译器可以帮你避免很多错误——也就是说可以防止你编译错误的代码。
相比之下,JavaScript 的类型是松散。程序员不确定他们收到的对象是什么类型,那么程序员怎么知道该怎么处理这个对象?
但是,Java 的严格类型检查同样导致了大量样板代码。程序员经常需要进行类型转换,或以其他方式确保一切都准确无误。程序员需要花很时间确保类型是准确的,所以使用更多的样板代码,希望通过及早捕获和修复错误来节省时间。
程序员不得不使用复杂的大型 IDE,仅仅使用简单的编辑器是不行的。IDE 为 Java 程序员提供了一些下拉列表,用于显示类的可用字段、描述方法的参数,帮助他们构建新的类和进行重构。
然后,你还得使用 Maven……
在 JavaScript 中,不需要声明变量的类型,所以通常不需要进行类型转换。因此,代码更易于阅读,但可能会出现未编译错误。
这一点会让你更喜欢 Java 还是痛恨 Java,取决于你自己。十年前,我认为 Java 的类型系统值得我们花费额外的时间,因为这样可以获得更多的确定性。但在今天,我认为代价太大了,使用 JavaScript 会要简单得多。
使用易于测试的小模块来扫除 bug
Node.js 鼓励程序员将程序划分为小单元,也就是模块。模块虽小,却能从一定程度上解决刚刚提到的问题。
一个模块应该具备以下特点:
自包含——将相关代码打包到一个单元中;
强壮的边界——模块内部的代码可以防止外部代码入侵;
显式导出——默认情况下,代码和模块中的数据不会导出,只将选定的函数和数据暴露给外部;
显式导入——声明它们依赖哪些模块;
可能是独立的——可以将模块公开发布到 npm 存储库或其他私有存储库,方便在应用程序之间共享;
易于理解——更少的代码意味着更容易理解模块的用途;
易于测试——小模块可以轻松进行单元测试。
所有这些特点组合在一起,让 Node.js 模块更容易测试,并具有明确定义的范围。
人们对 JavaScript 的恐惧源自它缺乏严格的类型检查,所以可能很容易导致错误。但在具有清晰边界的模块中,受影响代码被限于模块内部。所以,大多数问题被安全地隐藏在模块的边界内。
松散类型问题的另一个解决方案是进行更多的测试。
你必须将节省下来的一部分时间(因为编写 JavaScript 代码更容易)用在测试上。你的测试用例必须捕获编译器可能捕获的错误。
对于那些想要在 JavaScript 中使用静态检查类型的人,可以考虑使用 TypeScript。我没有使用 TypeScript,但听说它很不错。它与 JavaScript 兼容,同时提供了有用的类型检查和其他特性。
但我们的重点是 Node.js 和 JavaScript。
包管理
一想起 Maven 我就头大。据说一个人要么爱它,要么鄙视它,没有第三种选择。
问题是,Java 生态系统中并没有一个核心的包管理系统。Maven 和 Gradle 其实也很不错,但它们并不像 Node.js 的包管理系统那样有用、可用和强大。
在 Node.js 世界中,有两个优秀的包管理系统,首先是 npm 和 npm 存储库。
有了 npm,我们就相当于有了一个很好的模式用来描述包依赖性。依赖关系可以是严格的(指定具体的版本),或者使用通配符表示最新版本。Node.js 社区已经向 npm 存储库发布了数十万个包。
不仅仅是 Node.js 工程师,前端工程师也可以使用 npm 存储库。以前他们使用 Bower,现在 Bower 已被弃用,他们现在可以在 npm 存储库中找到所有可用的前端 JavaScript 库。很多前端框架,如 Vue.js CLI 和 Webpack,都是基于 Node.js 开发的。
Node.js 的另一个包管理系统是 yarn,它也是从 npm 存储库中拉取包,并使用与 npm 相同的配置文件。yarn 的主要优点运行得更快。
性能
曾几何时,Java 和 JavaScript 都因为运行速度慢而横遭指责。
它们都需要通过编译器将源代码转换为由虚拟机执行的字节码。虚拟机通常会进一步将字节码编译为本地代码,并使用各种优化技术。
Java 和 JavaScript 都有很大的动机让代码运行得更快。在 Java 和 Node.js 中,动机就是让服务器端代码运行得更快。而在浏览器端,动机是获得更好的客户端应用程序性能。
甲骨文的 JDK 使用了 HotSpot,这是一个具有多种字节代码编译策略的超级虚拟机。HotSpot 经过高度优化,可以生成非常快的代码。
至于 JavaScript,我们不禁在想:我们怎么能期望在浏览器中运行的 JavaScript 代码能够实现复杂的应用程序?基于浏览器 JavaScript 实现办公文档处理套件似乎是件不可能实现的事情?是骡子是马,拉出来溜溜就知道了。这篇文章是我用谷歌文档写的,它性能非常好。浏览器端 JavaScript 的性能每年都在飞涨。
Node.js 直接受益于这一趋势,因为它使用的是 Chrome 的 V8 引擎。
下面是 Peter Marshall 的演讲视频链接,他是谷歌的一名工程师,主要负责 V8 引擎的性能增强工作。他在视频中描述了为什么 V8 引擎使用 Turbofan 虚拟机替换了 Crankshaft 虚拟机。
V8 引擎中的高性能 JavaScript:
在机器学习领域,数据科学家通常使用 R 语言或 Python,因为他们十分依赖快速数值计算。但由于各种原因,JavaScript 在这方面表现很差。不过,有人正在开发一种用于数值计算的标准 JavaScript 库。
JavaScript 中的数值计算:
另一个视频演示了如何通过 TensorFlow.js 在 JavaScript 中使用 TensorFlow。它提供了一个类似于 TensorFlow Python 的 API,可以导入预训练模型。它运行在浏览器中,可用于分析实时视频,从中识别出经过训练的对象。
基于 JavaScript 的机器学习:
在另一个演讲视频中,IBM 的 Chris Bailey 讨论了 Node.js 的性能和可伸缩性问题,特别是在 Docker/Kubernetes 部署方面。他从一组基准测试开始,演示了 Node.js 在 I/O 吞吐量、应用程序启动时间和内存占用方面远远超过 Spring Boot。此外,得益于 V8 引擎的改进,Node.js 每次发布的新版在性能方面都有显著的提升。
Node.js 的性能和高度可伸缩的微服务:
在上面的这个视频中,Bailey 说我们不应该在 Node.js 中运行计算密集型的代码。因为 Node.js 采用了单线程模型,长时间运行计算密集型任务会导致事件阻塞。
如果 JavaScript 的改进还无法满足你的应用程序的要求,还有其他两种方法可以将本地代码直接集成到 Node.js 中。最直接的方法是使用 Node.js 本地代码模块。Node.js 工具链中包含了 node-gyp,可用于处理与本地代码模块的链接。下面的视频演示了如何集成 Rust 库和 Node.js:
JavaScript 与 Rust 集成,远比你想象得简单:
WebAssembly 可以将其他语言编译为运行速度非常快的 JavaScript 子集。WebAssembly 是一种可在 JavaScript 引擎内运行的可执行代码的可移植格式。下面的视频做了一个很好的概述,并演示了如何使用 WebAssembly 在 Node.js 中运行代码。
在 NodeJS 中使用 WebAssembly:
富 Internet 应用程序(RIA)
十年前,软件行业一直热议利用快速的 JavaScript 引擎实现富 Internet 应用程序,从而取代桌面应用程序。
这个故事实际上在二十多年前就已经开始了。Sun 公司和 Netscape 公司达成了共识,在 Netscape Navigator 中使用 Java 小程序(Applet)。
JavaScript 语言在某种程度上是作为 Java 小程序的脚本语言而开发出来的。服务器端有 Java Servlet,客户端有 Java Applet,这样就可以在两端使用同样的一门编程语言。然而,由于各种原因,这种美好的愿望并没有实现。
十年前,JavaScript 开始变得足够强大,可以实现复杂的应用程序。因此,RIA 被认为是 Java 客户端应用程序的终结者。
今天,我们开始看到 RIA 的想法得以实现。服务器端的 Node.js 和两端都有的 JavaScript 让这一切成为可能。
当然,Java 作为桌面应用程序平台的消亡并不是因为 JavaScript RIA,而是因为 Sun 公司忽视了客户端技术。Sun 公司把注意力放在要求快速服务器端性能的企业客户身上。当时我还在 Sun 公司任职,我亲眼看着这件事情发生。真正杀死 Applet 的是几年前在 Java 插件和 Java Web Start 中发现的一个安全漏洞。这个漏洞导致全球一致呼吁停止使用 Java Applet 和 Java Web Start 应用程序。
我们仍然可以开发其他类型的 Java 桌面应用程序,NetBeans 和 Eclipse IDE 之间的竞争仍然存在。但是,Java 在这个领域工作是停滞不前的,除了开发工具之外,很少有基于 Java 的应用程序。
JavaFX 是个例外
10 年前,JavaFX 意欲成为 Sun 公司对 iPhone 的反击。它用于开发基于 Java 的手机 GUI 应用程序,想把 Flash 和 iOS 应用程序打垮。然而,这一切都没有发生。JavaFX 现在仍然可以使用,但没有了当初的喧嚣。
这个领域的所有兴奋点都发生在 React、Vue.js 和类似的框架上,JavaScript 和 Node.js 在很大程度上要得益于此。
结论
现在,开发服务器端应用程序有很多选择。我们不再局限于“P”开头的语言(Perl、PHP、Python)和 Java,我们还有 Node.js、Ruby、Haskell、Go、Rust 等等。
至于为什么我会转向 Node.js,很明显,我更喜欢在使用 Node.js 编程时的那种自由的感觉。Java 成了负担,而 Node.js 没有这样的负担。如果我再次拿起 Java,那肯定是因为有人付了钱。
每个应用程序都有其真实需求。只是因为个人喜欢而一直使用 Node.js 也不见得是对的。在选择一门语言或一个框架时总归是有技术方面的考量的。例如,我最近完成的一些工作涉及 XBRL 文档,由于最好的 XBRL 库是用 Python 实现的,所以就有必要学习 Python。
欢迎工作一到五年的Java工程师朋友们加入Java技术交流:611481448
群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!
喜欢小编轻轻点个关注了哦!