抖音技术能力大揭密!钜惠大礼、深度体验,尽在火山引擎增长沙龙,就等你来! 立即报名>> 了解详情
写点什么

Java 11 : 无需编译即可运行单文件程序

2019 年 8 月 26 日

Java 11 : 无需编译即可运行单文件程序

本文要点

  • 该特性可以在无需编译的情况下,直接运行 Java 单文件源代码,避免了以前只运行一个简单的 Hello World 程序所涉及的繁琐步骤。

  • 这个特性对于那些想尝试简单程序或特性的 Java 新手来说特别有用;当我们将这个特性与 jshell 结合起来使用时,将会得到一个很棒的初学者学习工具集。

  • 该特性还能带来一些高级功能,我们可以通过命令行参数来控制它,处理更多的类,甚至可以在一次运行中向当前应用程序添加模块。

  • 将此特性与 Shebang 文件(#!)结合,通常,我们可以像使用命令行运行*nix bash 脚本那样, 将 Java 作为 shell 脚本运行。

  • 本文探讨了 Java 11+启动单文件源代码程序(JEP 330)的新特性,并提供了基于 JShell 的示例,这些示例分别展示了正确的和错误的用法及技巧。


我们为什么需要这个特性

如果我们回想一下 JavaSE 11(JDK 11)之前的日子,假设我们有一个 HelloUniverse.java 源文件,它包含一个类定义和一个静态的 main 方法,该方法打印一行文本到终端中,代码如下所示:


public class HelloUniverse{      public static void main(String[] args) {             System.out.println("Hello InfoQ Universe");      }}
复制代码


正常情况下,如果要运行这个类,首先,需要使用 Java 编译器(javac)来编译它,编译后将生成一个 HelloUniverse.class 文件:


mohamed_taman$ javac HelloUniverse.java
复制代码


然后,需要使用一条 java 虚拟机(解释器)命令来运行生成的字节码类文件:


mohamed_taman$ java HelloUniverseHello InfoQ Universe
复制代码


它将启动 JVM、加载类并执行代码。


但是,如果我们想快速测试一段代码,或者我们刚开始学习 Java(这里的关键词是 Java)并想实践这种语言,应该怎么办呢?上述过程中的两个步骤实践起来似乎还是有点难度。


在 Java SE 11 中,我们可以在无需任何中间编译的情况下,直接启动单个源代码文件。


这一特性对于那些想尝试简单程序的 Java 新手来说特别有用;当我们将这个特性与 jshell 结合起来使用时,我们将会得到一个很棒的初学者学习工具集。


更多关于**Jshell 10+**的新信息,请查看视频教程“Hands-on Java 10 Programming with JShell”。


专业人员也可以利用这些工具来探索新的语言变化或尝试未知的 API。在我看来,当我们可以自动化地执行很多任务时,比如,将 Java 程序编写为脚本,然后在操作系统 shell 中执行这些脚本,它将会产生更强大的功能。这种组合不仅为我们提供了 shell 脚本的灵活性,同时也提供了 Java 语言的强大功能。我们将在本文的第二部分更详细地探讨这个问题。


该 Java 11 特性的伟大之处在于,它使我们可以无需任何编译即可直接运行 Java 单文件源代码。现在让我们深入地了解它的更多细节和其他有趣的相关主题。


我们需要遵循什么

如果想要运行本文中提供的所有演示示例,我们需要使用 Java 的最新版本。它应该是 Java 11 或更高版本。当前的功能版本是 Java SE 开发工具包 12.0.1(最终版本可以从该链接获得,只需接受许可并单击与操作系统相匹配的链接即可)。如果想要了解更多的新特性,最新的 JDK 13 early access 是最近更新的,可以从这个链接下载。


我们还应该注意到,现在也可以从 Oracle 和其他供应商(如AdoptOpenJDK)处获取 OpenJDK 版本。


在本文中,我们使用纯文本编辑器而不是 Java IDE,因为我们想要避免任何 IDE 魔力,并在终端中直接使用 Java 命令行。


使用 Java 运行.java 文件

JEP 330启动单文件源代码程序Launch Single-File Source-Code Programs),是 JDK11 发行版本中引入的新特性之一。该特性允许我们直接使用 Java 解释器来执行 Java 源代码文件。源代码在内存中编译,然后由解释器执行,而不需要在磁盘上生成.class 文件了。


但是,该特性仅限于保存在单个源文件中的代码。不能在同一个运行编译中添加其他源文件。


为了满足这个限制,所有的类都必须在同一个文件中定义,不过它对文件中类的数量没有限制,并且类既可声明为公共类,也可以不是,因为只要它们在同一个源文件中就没关系。


源文件中声明的第一个类将被提取出来作为主类,我们应该将 main 方法放在第一个类中。所以类的顺序很重要。


第一个示例

现在,让我们以学习新东西时的一贯做法开始我们的学习吧,是的,你没有猜错,以一个最简单的“Hello Universe!” 示例开始。


我们将集中精力通过尝试不同的示例来演示如何使用该特性,以便你了解如何在日常编码中使用该特性。


如果还没有准备好,请先创建本文顶部列出的 HelloUniverse.java 文件,编译它,并运行生成的字节码类文件。


现在,我希望你删除编译生成的类文件;你马上就会明白为什么:


mohamed_taman$ rm HelloUniverse.class
复制代码


现在,如果不编译,只使用 Java 解释器运行该类,操作如下:


mohamed_taman$ java HelloUniverse.javaHello InfoQ Universe
复制代码


我们会看到它运行了,并返回和之前编译时相同的结果。


对于 java HelloUniverse.java 来说,我们传入的是源代码而不是字节码类文件,这就意味着,它在内部编译源代码,然后运行编译后的代码,最后将消息输出到控制台。


所以,它仍然需要进行一个编译过程,如果有编译错误,我们仍然会收到一个错误通知。此外,我们还可以检查目录结构,会发现并未生成字节码类文件;这是一个内存编译过程


现在,让我们看看这个魔法是如何发生的。


Java 解释器如何运行 HelloUniverse 程序

在 JDK 10 中,Java 启动程序会以如下三种模式运行:


  1. 运行字节码类文件

  2. 运行 JAR 文件中的 main 类

  3. 运行模块中的 main 类


现在,在 Java 11 中,又添加了一个新的第四模式:


  1. 运行源文件中声明的类


在源文件模式下,运行效果就像是,将源文件编译到内存中,并执行可以在源文件中找到的第一个类。


是否进入源文件模式由命令行上的如下两项来决定:


  1. 在命令行中既不是选项也不是选项一部分的第一项。

  2. 如果存在选项的话,它将是–source 选项。


对于第一种情况,Java 命令将查看命令行上的第一项,它既不是选项也不是选项的一部分。如果它有一个以.java 结尾的文件名,那么它将会被当作是一个要编译和运行的 Java 源文件。我们也可以在源文件名之前为 Java 命令提供选项。比如,如果我们希望在源文件中通过设置类路径来使用外部依赖项时。


对于第二种情况,选择源文件模式,并将第一个非选项命令行项视为要编译和运行的源文件。


如果文件没有.java 扩展名,则必须使用–source 选项来强制执行源文件模式。


当源文件是要执行的“脚本”,或者源文件的名称不遵循 Java 源文件的常规命名约定时,–source 选项是必要的。


–source 选项还可用于指定源代码的语言版本。稍后我会详细讨论。


我们可以传递命令行参数吗?

让我们丰富下“Hello Universe”程序,为访问 InfoQ Universe 的任何人创建一个个性化的问候:


public class HelloUniverse2{    public static void main(String[] args){        if ( args == null || args.length< 1 ){System.err.println("Name required");System.exit(1);        }  var name = args[0];  System.out.printf("Hello, %s to InfoQ Universe!! %n", name);    }}
复制代码


我们将代码保存在一个名为 Greater.java 的文件中。请注意,该文件的命名违反了 Java 编程规范,它的名称和公共类的名称不匹配。


运行如下代码,看看将会发生什么:


mohamed_taman$ java Greater.java "Mo. Taman"Hello, Mo. Taman to InfoQ universe!!
复制代码


我们可以看到的,类名是否与文件名匹配并不重要;它是在内存中编译的,并且没有生成 .class 文件。敏锐的读者可能还注意到了,我们是如何在要执行的文件名之后将参数传递给代码的。这意味着在命令行上文件名之后出现的任何参数都会以这种显式的方式传递给标准的 main 方法。


使用–source 选项指定代码文件的语言版本

有两种使用 --source 选项的场景:


  1. 指定代码文件的语言版本

  2. 强制 Java 运行时进入源文件执行模式


在第一种情况下,当我们缺省代码语言版本时,则假定它是当前的 JDK 版本。在第二种情况下,我们可以对除 .java 之外的扩展名文件进行编译并立即运行。


我们先研究一下第二个场景,将 Greater.java 重命名为没有任何扩展名的 greater,然后使用相同的方法,尝试再次执行它:


mohamed_taman$ java greater "Mo. Taman"Error: Could not find or load main class greaterCaused by: java.lang.ClassNotFoundException: greater
复制代码


正如我们所看到的那样,在没有 .java 扩展名的情况下,Java 命令解释器将以模式 1 的形式启动 Java 程序,它会根据参数中提供的文件名寻找编译后的字节码类。为了防止这种情况的发生,我们需要使用 --source 选项来强制指定源文件模式:


mohamed_taman$ java --source 11 greater "Mo. Taman"Hello, Mo. Taman to InfoQ universe!!
复制代码


现在,让我们回到第一个场景。Greater.java 类与 JDK 10 兼容的,因为它包含 var 关键字,但与 JDK 9 不兼容。将源版本更改为 10,看看会发生什么:


mohamed_taman$ java --source 10 Greater.java "Mo. Taman"Hello Mo. Taman to InfoQ universe!!
复制代码


现在再次运行前面的命令,但传递到 --source 选项的是 JDK 9 而不是 JDK 10:


mohamed_taman$ java --source 9 Greater.java "Mo. Taman"Greater.java:8: warning: as of release 10, 'var' is a restricted local variable type and cannot be used for type declarations or as the element type of an arrayvar name = args[0];            ^Greater.java:8: error: cannot find symbolvar name = args[0];        ^  symbol:   class var  location: class HelloWorld1 error1 warningerror: compilation failed
复制代码


请注意错误消息的形式,编译器警告说,在 JDK 10 中 var 会成为一个受限制的类型名,但是由于当前是 Java 语言 9 版本,所以编译仍会继续进行。但是,由于在源文件中找不到名为 var 的类型,所以编译失败。


很简单,对吧?现在让我们看看如何使用多个类。


它是否适用于多个类?

答案是肯定的。


让我们测试一段包含两个类的示例代码,以演示该特性可以适用于多个类。该代码的功能是检验给定的字符串是否为回文。回文可以是一个单词、短语、数字或其他字符序列,但它们从两个方向读取时,都能得到相同的字符序列,例如“redivider”或“reviver”。


如下是保存在名为 PalindromeChecker.java 文件中的代码:


import static java.lang.System.*;public class PalindromeChecker {      public static void main(String[] args) {                        if ( args == null || args.length< 1 ){                err.println("String is required!!");                exit(1);            }            out.printf("The string {%s} is a Palindrome!! %b %n",                  args[0],                  StringUtils                        .isPalindrome(args[0]));                  }}public class StringUtils {      public static Boolean isPalindrome(String word) {      return (new StringBuilder(word))            .reverse()            .toString()            .equalsIgnoreCase(word);      }}
复制代码


现在,我们运行一下这个文件:


mohamed_taman:code$ java PalindromeChecker.java RediVidErThe string {RediVidEr} is a Palindrome!! True
复制代码


使用“RaceCar”代替“RediVidEr”后,再运行一次:


mohamed_taman:code$ java PalindromeChecker.java RaceCarThe string {RaceCar} is a Palindrome!! True
复制代码


最后,再使用“Taman”来代替“RaceCar”:


mohamed_taman:code$ java PalindromeChecker.java TamanThe string {Taman} is a Palindrome!! false
复制代码


正如我们看到的那样,我们可以在单个源文件中添加任意多个的公共类。唯一的要点是,main 方法应该在源文件的第一个类中定义。解释器(Java 命令)将使用第一个类作为入口,在内存中编译代码并启动程序。


允许使用模块吗?

是的,完全允许使用模块。内存中编译的代码作为未命名模块的一部分运行,该未命名模块带有 --add-modules=ALL-DEFAULT 选项,该选项允许访问 JDK 附带的所有模块。


这使得代码可以使用不同的模块,而无需使用 module-info.java 显式声明依赖项。


让我们来看一些使用 JDK11 附带的新的 HTTP 客户端 API 进行 HTTP 调用的代码。注意,这些 API 是在 Java SE 9 中作为孵化器特性引入的,但是现在它们已经逐步发展成为 java.net.http 模块中的完整特性。


在本示例中,我们将通过 GET 方法调用一个简单的 REST API 来获取一些用户信息。我们将调用一个公共端点服务 https://reqres.in/api/users?page=2。示例代码位于名 UsersHttpClient.java 的文件中:


import static java.lang.System.*;import java.net.http.*;import java.net.http.HttpResponse.BodyHandlers;import java.net.*;import java.io.IOException;
public class UsersHttpClient{ public static void main(String[] args) throws Exception{var client = HttpClient.newBuilder().build(); var request = HttpRequest.newBuilder().GET().uri(URI.create("https://reqres.in/api/users?page=2")).build();
var response = client.send(request, BodyHandlers.ofString());out.printf("Response code is: %d %n",response.statusCode());out.printf("The response body is:%n %s %n", response.body()); }}
复制代码


运行程序,将产生如下的输出结果:


mohamed_taman:code$ java UsersHttpClient.javaResponse code is: 200The response body is:{"page":2,"per_page":3,"total":12,"total_pages":4,"data":[{"id":4,"first_name":"Eve","last_name":"Holt","avatar":"https://s3.amazonaws.com/uifaces/faces/twitter/marcoramires/128.jpg"},{"id":5,"first_name":"Charles","last_name":"Morris","avatar":"https://s3.amazonaws.com/uifaces/faces/twitter/stephenmoon/128.jpg"},{"id":6,"first_name":"Tracey","last_name":"Ramos","avatar":"https://s3.amazonaws.com/uifaces/faces/twitter/bigmancho/128.jpg"}]}
复制代码


这允许我们快速测试不同模块提供的新功能,而无需创建自己的模块。


更多关于新的 Java 平台模块系统(JPMS)的信息,请查看视频教程“Getting Started with Clean Code Java SE 9”。


为什么脚本对 Java 来说很重要?

首先,让我们回顾一下脚本是什么,以便于理解为什么在 Java 编程语言中使用脚本如此重要。


我们可以给脚本作如下的定义:


脚本是为特定的运行时环境编写的程序,它可以自动执行任务或命令,这些任务或命令也可以由操作人员逐个执行。


在这个通用定义中,我们可以推导出脚本语言的一个简单定义;脚本语言是一种编程语言,它使用高级构造器每次解释并执行一个命令。


脚本语言是一种编程语言,它在文件中使用一系列命令。通常,脚本语言是解释语言(而不是编译语言),并且倾向于过程式编程风格(尽管一些脚本语言也有面向对象的特性)。


一般来说,脚本语言比更结构化的编译语言(如 Java、C 和 C++)更容易学习,也能更快地进行代码编写。服务端的脚本语言有 Perl、PHP 和 Python 等,客户端的脚本语言有 JavaScript。


长期以来,Java 被归类成一种结构良好的、强类型的编译语言,经 JVM 解释运行于任何计算机体系结构上。然而,对于 Java 的一个抱怨是,与普通脚本语言相比,它的学习及原型开发速度不够快。


然而,现在,Java 已经成为一门历经 24 年的语言,全世界大约有 940 万的开发人员在使用它。为了让年轻一代的程序员更容易地学习 Java,并在不需要编译和 IDE 的情况下尝试其特性和 API,Java 最近发布了一些特性。从 Java SE 9 开始,添加了一个支持交互式编程的 JShell (REPL) 工具集,其目的就是使 Java 更易于编程和学习。


现在,使用 JDK 11,Java 逐步成为一种支持脚本的编程语言,因为我们可以简单地通过调用 Java 命令来运行代码了!


在 Java 11 中,有两种基本的脚本编写方法:


  1. 直接使用 java 命令工具。

  2. 使用 *nix  命令行脚本,它类似于 bash 脚本


我们已经探讨过了第一种方法了,所以现在是时候看一下第二种方法,这是一个可以打开许多可能性大门的特性。


Shebang 文件:以 shell 脚本的形式运行 Java

如前所述,Java SE 11 引入了对脚本的支持,包括支持传统的*nix,即所谓的 Shebang 文件。无需修改 JLSJava Language Specification,Java 语言规范)就可以支持该特性。


在一般的 Shebang 文件中,前两个字节必须是 0x230x21 ,这是"#!"两个字符的 ASCII 编码。然后,才能有效地使用默认平台字符编码读取文件所有后续字节。


因此,当希望使用操作系统的 Shebang 机制执行文件时,文件的第一行需要以 #!开始。这意味着,当显式使用 Java 启动程序运行源文件代码时,无需任何特殊的第一行,比如上面的 HelloUniverse.java 示例。


让我们在 macOS Mojave 10.14.5 的终端中运行下一个示例。但是首先,我们需要列出一些创建 Shebang 文件时,应该遵循的重要规则:


  • 不要混合使用 Java 代码与操作系统的 shell 脚本语言。

  • 如果需要包含 VM(虚拟机)选项,则必须将 --source 指定为 Shebang 文件可执行的文件名后面的第一个选项。这些选项包括:–class-path、–module-path、–add-exports、–add-modules、–limit-modules、–patch-module、upgrade-module-path ,以及这些选项的任何变体形式。它还可以包括 JEP 12 引入的新的–enable-preview 选项。

  • 必须为文件中的源代码指定 Java 语言版本。

  • Shebang 字符(#!)必须在文件的第一行,它应该是这样的:


#!/path/to/java --source <version>
复制代码


  • 不允许使用 Shebang 机制来执行遵循标准命名约定(以 .java 结尾的文件)的 Java 源文件。

  • 最后,必须使用以下命令将文件标记为可执行文件:


chmod +x <Filename>.<Extension>.
复制代码


在我们的示例中,我们创建一个 Shebang 文件(script utility program),它将列出作为参数传递的目录内容。如果没有传递任何参数,则默认列出当前目录。


#!/usr/bin/java --source 11import java.nio.file.*;import static java.lang.System.*;
public class DirectoryLister { public static void main(String[] args) throws Exception { vardirName = ".";
if ( args == null || args.length< 1 ){err.println("Will list the current directory"); } else { dirName = args[0]; }
Files .walk(Paths.get(dirName)) .forEach(out::println); }}
复制代码


将此代码保存在一个名为 dirlist 文件中,它不带任何扩展名,然后将其标记为可执行文件:


mohamed_taman:code$ chmod +x dirlist
复制代码


按以下方式运行:


mohamed_taman:code$ ./dirlistWill list the current directory../PalindromeChecker.java./greater./UsersHttpClient.java./HelloWorld.java./Greater.java./dirlist
复制代码


通过传递父目录,按照如下命令再次运行程序 ,并检查它输出。


mohamed_taman:code$ ./dirlist ../
复制代码


注意:在计算源代码时,解释器会忽略 Shebang 行(第一行)。因此,启动程序也可以显式地调用 Shebang 文件,可能需要使用如下附加选项:


$ java -Dtrace=true --source 11 dirlist
复制代码


另外,值得注意的是,如果脚本文件在当前目录中,还可以按以下方式执行:


$ ./dirlist
复制代码


或者,如果脚本在用户路径的目录中,也可以这样执行:


$ dirlist
复制代码


最后,我们通过展示一些使用该特性时需要注意的用法和技巧来结束本文。


用法和技巧

  1. 可以传递给 javac 的一些选项可能不会被 Java 工具所传递(或识别),比如, -processor 和 -Werror 选项。

  2. 如果类路径中同时存在.class 和.java 文件,启动程序将强制使用字节码类文件。


mohamed_taman:code$ javac HelloUniverse.javamohamed_taman:code$ java HelloUniverse.javaerror: class found on application class path: HelloUniverse
复制代码


请记住类和包存在命名冲突的可能性。请看如下的目录结构:


mohamed_taman:code$ tree.├── Greater.java├── HelloUniverse│   ├── java.class│   └── java.java├── HelloUniverse.java├── PalindromeChecker.java├── UsersHttpClient.java├── dirlist└── greater
复制代码


注意:HelloUniverse 包下的两个 java.java 文件和当前目录中的 HelloUniverse.java 文件。当我们试图运行如下命令时,会发生什么呢?


mohamed_taman:code$ java HelloUniverse.java
复制代码


运行哪个文件,第一个还是第二个?Java 启动程序不再引用 HelloUniverse 包中的类文件。相反,它将通过源代码模式加载并运行 HelloUniverse.java 文件,以便运行当前目录中的文件。


我喜欢使用 Shebang 特性,因为它为利用 Java 语言的强大功能来创造脚本自动化完成大量工作提供了可能性。


总结

从 Java SE 11 开始,在这款编程语言的历史上,首次可以在无需编译的情况下,直接运行包含 Java 代码的脚本。Java 11 源文件执行特性使得使用 Java 编写脚本并直接使用 *inx 命令行执行脚本成为可能。


今天就开始尝试使用这个新特性吧,祝大家编程愉快。如果喜欢这篇文章,请将它分享给更多的极客。


参考资源


作者介绍

Mohamed Taman 是 @DevTech d.o.o 的高级企业架构师、Java 冠军、甲骨文开拓大使、Java SE.next()和 JakartaEE.next()的采纳者、JCP 成员。他曾是 JCP 执行委员会成员、JSR 354、363、373 专家组成员、EGJUG 领导者、甲骨文埃及架构师俱乐部董事会成员。他主讲 Java,热爱移动、大数据、云、区块链、DevOps。他是国际讲师,是“JavaFX essentials”、“Getting Started with Clean Code, Java SE 9”、“Hands-On Java 10 Programming with JShell” 等书和视频的作者。还出了一本新书“Secrets of a Java Champions”。他还赢得过 2014、2015 年杜克选择奖项和 JCP 杰出参与者 2013 年奖项。


原文链接:


https://www.infoq.com/articles/single-file-execution-java11/


2019 年 8 月 26 日 08:008758
用户头像

发布了 148 篇内容, 共 61.8 次阅读, 收获喜欢 386 次。

关注

评论 1 条评论

发布
用户头像
感觉并没有那么好用
2019 年 08 月 26 日 09:32
回复
没有更多了
发现更多内容

详解HDFS3.x新特性-纠删码

五分钟学大数据

hadoop hdfs

JavaScript02 - js的引入方式

桃夭十一里

JavaScript

电商网站商品管理(二)多种搜索方式

escray

elasticsearch elastic 28天写作 死磕Elasticsearch 60天通过Elastic认证考试

APICloud AVM多端开发 |《生鲜电商app开发》项目源码教程

APICloud

前端开发 移动开发 APP开发 APICloud

[5/28]产品运维保障体系的质量实践

俊毅

阿里表哥甩我一份Redis笔记,看完还进不了阿里让我卖豆腐去

互联网架构师小马

Java 数据库 nosql redis 面试

我们为什么打比方

石云升

28天写作 确认偏误 打比方

限时开放!阿里P8大师终于把这份微服务架构与实践第2版PDF分享出来了

云流

Java 编程 程序员 微服务 架构师

技术人员如何写好周报

猿话

2021字节、华为、滴滴Java内部面试题(含答案),新鲜出炉!

比伯

Java 编程 架构 面试 程序人生

案例研究之聊聊 QLExpress 源码 (七)

小诚信驿站

聊聊架构 规则引擎 28天写作 QLExpress源码 聊聊源码

架构师第八周总结

Geek_xq

【得物技术】代码覆盖率原理与得物app实践

得物技术

测试 原理 代码 得物技术 覆盖率

Python列表对象入门

赵开忠

28天写作

一文带你学会AQS和并发工具类的关系

伯阳

AQS java 并发 ReentrantLock 多线程高并发 lock锁

超越身边80%的人,其实没有你想象的那么难

架构精进之路

认知提升 成长笔记 七日更 28天写作

也谈Python编码格式

ITCamel

Python 编码格式

这份30天获得40k+星,多次登上榜首的算法宝典,带你刷爆LeetCode

Crud的程序员

程序员 架构 算法

Java并发编程实战(4)- 死锁

技术修行者

Java 并发编程 多线程 死锁

技术创新是PC市场发展基石,英特尔占据明显领先优势

新闻科技资讯

为什么印度不会成为世界工厂?

JiangX

印度 28天写作 世界工厂

使用 kubectl-rabbitmq 部署和运维 K8S 上的 RabbitMQ 集群

郭旭东

RabbitMQ kubectl kubectl plugin

IO和NIO的对比篇

Java架构师迁哥

使用nodejs和express搭建http web服务

程序那些事

HTTP nodejs 异步IO 程序那些事 web服务

Spring Boot 集成Thymeleaf模板引擎

武哥聊编程

Java springboot SpringBoot 2 thymeleaf 28天写作

JavaScript01 - 基础

桃夭十一里

JavaScript

JavaScript03 - window对象的方法

桃夭十一里

JavaScript

区块链2021狂想曲:迎接以技术为名的春天

脑极体

9. 细节见真章,Formatter注册中心的设计很讨巧

YourBatman

Converter ConversionService Formatter

在GitHub中向开源项目提交PR的过程

worry

GitHub pull request

自动驾驶分级,小白能理解的那种(28天写作 Day8/28)

mtfelix

自动驾驶 28天写作

Study Go: From Zero to Hero

Study Go: From Zero to Hero

Java 11 : 无需编译即可运行单文件程序-InfoQ