关注开源中国OSC头条号,获取最新技术资讯
参与翻译 (6人) : 边城, 硅谷课堂, liyue李月, 子影, ZICK_ZEON, Tocy
直到最后一口气,在Sun Microsystems的Java SE团队工作10年以上的人难道不应该流出Java字节码并实例化抽象接口么?对于这位前Java SE团队成员来说,2011年学习Node.js平台是一股清流。在2009年1月从Sun被解雇之后(就在Oracle收购之前),我学习了Node.js并且迷上了它.
怎么迷上了?自2010年以来,我撰写了大量关于Node.js编程文章。即,Node.js Web开发的四个版本,以及关于Node.js编程的其他书籍和众多教程博客文章。这些文章很多时间解释了Node.js和JavaScript语言的进步。
在Sun Microsystems工作期间,我相信一切皆java。我出席过JavaONE会议,共同开发了java.awt.Robot类,运行了Mustang回归竞赛(Java 1.6版本的漏洞发现竞赛),帮助推出了OpenJDK之前的“Distributions License for Java”回答Linux发行版分发JDK版本,后来在启动OpenJDK项目中扮演了一个小角色。在此过程中,我在java.net(一个现已解散的网站)上发布了一个博客,每周写1-2次,讨论Java生态系统中的事件约6年。一个重要的主题是保护Java免受那些预测Java死亡的人的影响。
杜克奖颁是颁给哪些表现超越的员工。我在运行野马回归竞赛后获得了这个奖项,这个发现bug竞赛有利于Java 1.6版本发布。
Java元码发生了什么?我在这里的目的是解释这个纯粹的Java倡导者是如何成为一个纯粹的Node.js / JavaScript倡导者。
我并没有完全脱离Java。在过去的3年中,我编写了大量的Java/Spring/Hibernate代码。当我完全享受这项工作的时候——我在太阳能行业工作过,做过一些让人精神振奋的事情,比如编写关于千瓦小时的数据库查询——用Java编程已经失去了它的光彩。
基于 Spring 的两年编程经验带给我一个非常深刻的教训:掩盖复杂性并不能造就简单,那只会让事件变得更复杂。
TL;DR
Java 中充斥着样板代码,他们掩盖了程序员的真实意图Spring & Spring Boot 带来的教训:掩盖复杂只会变得更复杂Java EE 就是个”设计委员会“项目,它涵盖了企业应用开发的一切事务,复杂无比Spring 的编程体验非常棒,直到有一天,一个莫名其妙的异常从一个你一点也不了解的深层子系统中冒出来,让你花了至少 3 天来搞明白遇到了什么问题零代码框架的会带来怎样的代价?像 Eclipse 这样的 IDE 非常强大,但它也揭示了 Java 的复杂性Node.js 是从轻量事件驱动架构中提炼出来的产物JavaScript 社区似乎很乐意抛开样板代码,让程序员们的思想自由闪耀用于解决回调地狱的 async/await 函数就是一个去除样板,拥抱创造的例子使用 Node.js 编程让人身心愉悦JavaScript 缺乏像 Java 那样严格的类型检查,这有利有弊,因为写代码变得容易了,但却需要更多测试来保证其正确性npm/yarn 包管理系统非常棒,非常好用,比可恶的 Maven 不知道好到哪儿去了Java 和 Node.js 都有出色的性能。并不是不像有人说的那样,因为 JavaScript 慢,所以 Node.js 性能低下Node.js 的性能利益于 Google 为了给 Chrome 提速而投入开发的 V8浏览器之间的激烈竞争使得 JavaScript 一年比一年强大,这是 Node.js 的福音
Java已经成为一个负担,使用Node.js是充满快乐的
有些工具或物件是设计师花了数年打磨和改进的结果。他们尝试了不同的想法,删除了不必要的属性,最终得到了一个对象,该对象的属性恰好合适。通常这些对象有一种强大的简单性,非常吸引人。Java不是那种系统。
Spring是开发基于java的web应用程序的流行框架。Spring(特别是Spring Boot)的核心目的是使用预先配置的Java EE堆栈。Spring程序员不需要连接所有servlet、数据持久化、应用服务器,谁知道还有什么,就可以获得完整的系统。相反,Spring负责处理所有这些细节,而您则专注于编码。例如,JPA Repository类用“findUserByFirstName”之类的名称来综合数据库查询方法——您不需要编写任何代码,只需将以这种方式命名的方法添加到存储库定义中,Spring就会处理剩下的问题。
这是一个很棒的故事,也是一次很好的经历,直到它没有了。
当您得到关于“传递给持久化的分离实体”的Hibernate PersistentObjectException时,这是什么意思?这花费了好几天的时间——冒着过于简化的风险——这意味着到达REST端点的JSON具有带值的ID字段。Hibernate,重载,想要控制ID值,并抛出这个令人困惑的异常。有成千上万同样令人困惑和迟钝的异常消息。在Spring堆栈中一个接一个的子系统中,就像一个复仇女神坐在那里等着你犯最小的错误,然后用一个应用程序崩溃异常来攻击你。
然后是巨大的堆栈跟踪。他们在几个屏幕上展示了很多抽象的方法这个和那个。Spring显然正在制定实现代码内容所需的配置。这个抽象级别显然需要相当多的逻辑来查找所有内容并执行请求。长堆栈跟踪并不一定是坏事。相反,它指出了一个症状:内存/性能开销成本到底是多少?
零编程的情况下,“findUserByFirstName” 会怎样执行?框架必须解析方法名,猜测程序员的意图,构造一些类似于抽象语法树一样的东西,生成 SQL 等等。这会带来什么样的开销?难道这样程序员就不需要写代码了吗?
有过数次这样的经历后,你需要花数周时间去学习原本不需要学习的深奥知识,然后得出和我一样的结论:掩盖复杂性并不会让事情变得简单,只会让事情变得更复杂。
重点关注下 Node.js
“兼容性问题”是个非常酷的口号,它表示 Java 平台的关键价值主张是完全向后兼容。我们对此非常认真,甚至把它像上图一样印在 T 恤上。保持这种程度的兼容性可能会是个非常沉重的负担,而有时候避免使用陈旧无用的方法,本身很有效。
Node.js 的另一面…
相比于 Spring 和 Java EE 的异常复杂,Node.js 是一股清流。首先是 Ryan Dahl 在开发 Node.js 核心平台所用的设计美学。Dahl 的经验是在重量级复杂系统中使用线程。他寻求不同的东西,并花了几年时间打磨和改进了一系列核心思想,并将之在 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命令。异步库非常适合简化异步执行的顺序。但它的使用需要一堆模板代码来模糊程序员的意图。
这里有一个循环。它不是作为循环编写的,并且它不使用自然循环结构。此外,错误和结果不会落在自然的地方,但是不方便地被困在回调函数内。在ES2015 / 2016功能登陆Node.js之前,这是我们能做的最好的事情。
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,就不能在异步函数中干净利用。
回调问题并没有通过复杂性来解决。相反,语言和范式的变化通过临时解决方案解决了这些问题和强加给我们的过度措辞。使用异步函数,我们的代码变得更加美观。
虽然这最初是针对Node.js的,但优秀的解决方案将其转换为Node.js和JavaScript。
通过定义明确的类型和接口来假装一切尽在掌握
我认为 Java 提倡通过强类型检查来保障大型应用开发这一说法就是作死。这一准则提出来的时候大家还在开发单体系统(没有微服务,也没有 Docker 等)。因为 Java 有严格的类型检查,Java 编译器可以避免编译不好的代码,以此帮助你避免多种缺陷的发生。
相对而言,JavaScript 拥有弱类型。道理很简单:既然程序员们不知道得到的是什么类型的对象,他们又怎么知道能干什么?
Java 强类型带来的是死板。程序员们不断地通过编写代码和其它一些事情来保证一切都确确实实正确。他们花时间极其精确地、死板地编写代码,希望通过更早的发现并修复错误,以便节约时间。
还有一个大问题,人们或多或少需要使用一个庞大而复杂的 IDE。简单的程序员编辑器完全不够用。要想保持程序员的思路清晰(除了披萨),需要大量的工具,比如下拉显示对象中的有效字段,描述方法的参数,辅助构建对象,辅助重构,以及其它由 Eclipse、NetBeans 和 IntelliJ 提供的各种工具。
还有……不要让我用 Maven,那个工具简直可以用恐怖来形容。
JavaScript 不需要声明变量类型,类型转换基本不会用到。因此,代码读起来非常清晰,不过却存在潜藏错误的风险。
在这一点上,同否赞同 Java 的作法由你决定。我在十年前认为获取更多确定性的开销是值得的。但现在我认为那样做的代价太大,还是用 JavaScript 的方式做事要容易得多。
通过小测试剔除模块的缺陷
Node.js 鼓励程序员把程序拆分成较小的单元 —— 模块。这看起来是件小事,但它在一定程度上让标题描述的事情变成为可能。
模块有如下特点:
自包含?—顾名思义,它会把相关的代码打包到一个单元中强边界—?模块内的代码不会受到其它代码侵入精确导出?—默认情况下,模块中的代码和数据并不对外开放,只有指定的函数和数据才会导出精确导入?—模块会声明其依赖哪些模块潜在独立性?—模块很容易公开发布到 npm 库,也可以私有发布到任何地方,以便在应用程序之间共享更容易推断?—?阅读更少代码,可以更容易搞明白共目的更容易测试 —?小模块更容易进行单元测试在判断其是否正确
所有这些决定了 Node.js 易于定义良好的范围,以及测试。
JavaScript令人担忧的是缺少强类型代码检查,这会很容易造成一些错误。在一个小的,边界清晰的焦点模块中,受影响的代码范围主要受限于该模块。这使得大多数关注点很小,且可以安全的集成要该模块中。
解决问题的另一个解决办法是增强测试。
你必须花费一些生产所得收益(这对于 JavaScript 编码来说是很容易的)去增强测试。你的测试规范必须能捕捉到编译器可能已经捕捉到的错误。你会测试你的代码吗?
对于那些想要在 JavaScript 中使用静态检查类型的人,可以去看看 TypeScript 。我从没用过这个语言,但是听别人说过关于它的一些很棒的事情。它直接与 JavaScript 兼容,且添加了许多有用的类型检查和其它特性。
这里的重点是 Node.js 和 JavaScript 。
包管理
我光是考虑 Maven 就会得了中风,并且根本无法思考直接写出关于它的任何东西。据说一个人要么爱 Maven ,要么鄙视它,没有中间立场。
问题在于 Java 生态系统没有一个具有凝聚力的包管理系统。Maven 包存在并且工作得相当好,据说也可以在 Gradle 中工作。但它并不像 Node.js 的包管理系统那样有用/可用/强大。
在 Node.js 的世界里,有两个优秀的包管理系统紧密协作。首先,npm 和 npm 存储库是唯一的工具。
使用 npm ,我们有一个很好的模式来描述包的依赖。依赖关系可以是严格的(确切地说是版本1.2.3)或者指定为“*”,表示使用的是最新版本。Node.js 社区已经将数十万个包发布到了 npm 存储库中。使用 npm 存储库之外的包就和使用 npm 存储库的包一样简单。
npm 库不仅服务于 Node.js,也服务于前端工程师,这再好不过了。以前我们使用像 Bower 这样的工具进行包管理。Bower 已经不再推荐使用,现在的前端开发者会在 npm 中去寻找前端 JavaScript 库。很多前端工具链,比如 Vue.js CLI 和 Webpack,都是 Node.js 写的。
yarn 是另一个 Node.js 的包管理系统,它可以从 npm 库下载所需要的包,使用与 npm 相同的配置文件。重要的是,yarn 运行更快。
npm 库可以通过 npm 或 yarn 来访问,它是 Node.js 轻松易用的有力保障。
在协助创建了 java.awt.Robot 之后,我受到启发创建了这一工具。官方的 Duke 吉祥物完全由曲线绘制,RoboDuke 除了传动部分和肘部分,其它部分都是直线。
性能
有时候 Java 和 JavaScript 都被认为运行缓慢。
它们都需要编译器将源代码转换为由虚拟机(VM)执行的字节码。VM 经常会进一步将字节码编译成本地代码,并使用各种优化技术。
无论 Java 还是 JavaScript 有都巨大的理由来快速运行。对于 Java 和 Node.js 来说,快速的服务端代码就是这样的需求。而在浏览器中,JavaScript 需要更好的客户端应用性能 —— 下一节 RIA 中会对此详述。
Sun/Oracle JDK 使用 HotSpot,一个具有多重字节码编译策略的超强虚拟机。它的名字代表着它会检查频繁执行的代码并逐步加强优化以执行更多代码段。HtoSpot 会调试优化,产生非常快的代码。
对于 JavaScript 来说,我们常常会想:我们要怎样让浏览器中的 JavaScript 跑得动各种复杂的应用程序?当真办公文档处理套件不能用 JavaScript 在浏览器上实现吗?我们让事实来说话。我现在正用 Google Docs 写这篇文章,性能真心不错。浏览器上的 JavaScript 性能每年都是飞速进步。
Node.js 直接受益于这种趋势,因为它使用 Chrome 的 V8 引擎。
Peter Marshall 说到一个例子,Google V8 工程师的主要工作就是提高 V8 的性能。Marshall 的工作是处理 Node.js 的性能问题。他详述了为什么要从 Crankshaft 虚拟机切换到 Turbofan 虚拟机。
https://youtu.be/YqOhBezMx1o
机器学习这一领域涉及到大量数学知识,而数学科学家样常用 R 和 Python。包括机器学习在内的若干领域依赖于快速数值计算。由于各种原因,JavaScript 在这方面的较为落后,但目前 JavaScript 在数值计算方面的标准库正在开发中。
https://youtu.be/1ORaKEzlnys
另一个视频演示了在 JavaScript 中使用 TensorFlow,它使用了 TensorFLow.js 库。这个库的 API 与 TensorFlow Python 相似,可以导入预训练模块。它可以用于分析实时视频以识别训练过的对象,而且可以完全在浏览器中运行。
https://youtu.be/YB-kfeNIPCE
在另一场谈话中,IBM 的 Chris Baily 谈到了 Node.js 的性能和伸缩性相关的问题,尤其是与 Docker/Kunbernetes 部署相关的问题。他从一组基准测试开始,表明 Node.js 在 I/O 吞吐量、程序启动时间和内存占用等方面显著优于 Spring Boot。此外,随着 V8 性能的提升,Node.js 每一个发行版都得到了令人瞩目的提升。
https://youtu.be/Fbhhc4jtGW4
视频中,Bailey 认为不应该在 Node.js 中运行计算代码。理解他为什么这么认识非常重要。因为单线程模型长时间运行计算会阻塞事件执行。我在《Node.js Web 开发》一书中谈到这一问题,并展示了三种处理办法:
算法重构 —— 检测算法中较慢的问题,通过重构提速通过事件调试拆分计算,以便 Node.js 定期返回执行线程将计算移到后端服务中去
如果 JavaScript 的改进仍不满足于你的应用需求,还有两个办法在 Node.js 中直接整合本地代码。最直接的方法是 Node.js 的本地代码模块。Node.js 的工具链中有有个 node-gyp 用于处理对本地代码的链接。下面的视频演示了将 Rust 整合到 Node.js 中:
https://youtu.be/Pfbw4YPrwf4
WebAssembly 可以将其它语言编译成运行速度非常快的 JavaScript 子集。WebAssembly 是可以在 JavaScript 引擎中执行的便携格式。下面这个视频很好地概述了这一技术,并演示了在 Node.js 中运行 WebAssembly。
https://youtu.be/hYrg3GNn1As
丰富的因特网应用(Rich Internet Applications,RIA)
十年前软件业热衷于使用快速(时间上的)JavaScript 引擎实现 RIA,这让桌面应用的地位受到威胁。
这故事要追溯到 20 年前。Sun 和 Netscape(网景)达成了在 Netscape Navigator(网景浏览器)中使用 Java Applet 的协议。JavaScript 在某种程度上被设计为 Java Applet 的脚本语言。当时希望在 Java 在服务端有 Servlet,而在客户端用 Applet,两端使用相同的编程语言,在和谐的环境中开发。但最终由于种种原因未能成型。
十年前 JavaScript 自己也开始能实现足够复杂而强大的应用。因此 RIA 作为一个可以干掉 Java 的客户端应用平台而流行起来。
现在我们开始看到 RIA 理念成为现实。随着 Node.js 出现,前后端都可以使用 JavaScript,20 年前的美好愿景得以实现。
一些实例:
Google Docs (写文章的东西)看起来和原来的 Office 套件一样,但它基于浏览器运行像 React、Angular 和 Vue 这样的强大框架,让使用 HTML/CSS 技术的浏览器应用开发变得简单Electron 整合了 Node.js 和 Chrominum 浏览器,可以支持跨平台的桌面应用开发。很多有名的应用,比如 Visual Studio Code、Atom、GitKraken,以及 Postman 等,都是基于 Electron 来编写的。它们的表现都很不错。由于 Electron/NW.js 使用浏览器引擎,像 React/Angular/Vue 这样的框架得以应用于桌面应用开发。比如这个示例:https://blog.sourcerer.io/creating-a-markdown-editor-previewer-in-electron-and-vue-js-32a084e7b8fe
Java 桌面应用平台并不是因为 JavaScript RIA 而消亡,而是因为 Sun Microsystems 公司在客户端技术方面的疏忽。Sun 把精力放在企业用户要求的高速服务端性能上。真正导致 Java Applet 消亡的是几年前 Java 插件和 Java Web Start 中发现的一个严重的安全漏洞。这个漏洞引起了全世界的警惕,让大家直接了当的不再使用 Java Applet 和 Java Web Start 应用。
不过仍然可以开发其他类型的 Java 桌面应用,因此 NetBeans 和 Eclipse 这两大 IDE 仍在激烈竞争。不过 Java 在这方面的工作没多大进展,除了开发工具外,很少有基于 Java 的应用。
JavaFX 是个例外。
JavaFX 在 10 年前被 Sun 用于 iPhone。它准备在手机上通过 Java 平台支持开发界面丰富的 GUI 应用,并以此排挤 Flash 和 iOS 应用开发。当然这件事情最终没有发生。JavaFX 仍在使用,却不是按照它宣传的那样。
这一领域所有让人感到兴奋的东西都与 React、Vue.js 这些框架相关。
这种情况之下,JavaScript 和 Node.js 从很大程度上来说是胜利者。
Java 指环曾在 Java ONE 大会分发。这种指环含有一个带有完整 Java 实现的芯片。其主要作用是在 JavaONE 大会上解锁我们安装在大厅的计算机。
Java 指环介绍。
总结
如今,开发服务器端代码有很多选择。我们不再局限于“P语言”(Perl、PHP、Python)和Java,再加上有Node.js, Ruby, Haskell, Go, Rust等等这些编程语言的存在。因此,我们要享受选择过多带来的尴尬。
为什么这个写Java代码的家伙会转向Node。很明显,我更喜欢用Node.js编程时那种自由的感觉。Java成为一个负担,用Node.js。没有这样的负担。然而假设我再次被雇佣写Java代码,我当然还会干,也单单因为别人花钱雇了我所以我不得不干。
每个应用程序都有其特定的需求。就因为一个人喜欢Node.js就总使用他当然是不正确的。选择一种语言或框架而不是另一种语言或框架一定有技术原因。例如,我最近做的一些工作涉及到XBRL文档。因为最好的XBRL库是在Python中实现的,所以有必要学习Python来继续这个项目。实事求是地评估你的真实需求,并做出相应的选择吧。
开源社区OSC「好文翻译」栏目,旨在每天为用户推荐并翻译优质的外网文章。再也不用怕因为英语不过关,被挡在许多技术文章的门外。关注开源社区OSC,每日获取翻译好文推荐,点击“了解更多”,阅读原文章。 |