本文要點
- 該特性可以在無需編譯的情況下,直接運行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 HelloUniverse
Hello 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.java
Hello InfoQ Universe
我們會看到它運行了,並返回和之前編譯時相同的結果。
對於 java HelloUniverse.java 來說,我們傳入的是源代碼而不是字節碼類文件,這就意味着,它在內部編譯源代碼,然後運行編譯後的代碼,最後將消息輸出到控制檯。
所以,它仍然需要進行一個編譯過程,如果有編譯錯誤,我們仍然會收到一個錯誤通知。此外,我們還可以檢查目錄結構,會發現並未生成字節碼類文件;這是一個內存編譯過程。
現在,讓我們看看這個魔法是如何發生的。
Java解釋器如何運行HelloUniverse程序
在JDK 10中,Java啓動程序會以如下三種模式運行:
- 運行字節碼類文件
- 運行JAR文件中的main類
- 運行模塊中的main類
現在,在Java 11中,又添加了一個新的第四模式:
- 運行源文件中聲明的類
在源文件模式下,運行效果就像是,將源文件編譯到內存中,並執行可以在源文件中找到的第一個類。
是否進入源文件模式由命令行上的如下兩項來決定:
- 在命令行中既不是選項也不是選項一部分的第一項。
- 如果存在選項的話,它將是–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 選項的場景:
- 指定代碼文件的語言版本
- 強制Java運行時進入源文件執行模式
在第一種情況下,當我們缺省代碼語言版本時,則假定它是當前的JDK版本。在第二種情況下,我們可以對除 .java 之外的擴展名文件進行編譯並立即運行。
我們先研究一下第二個場景,將 Greater.java 重命名爲沒有任何擴展名的 greater,然後使用相同的方法,嘗試再次執行它:
mohamed_taman$ java greater "Mo. Taman"
Error: Could not find or load main class greater
Caused 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 array
var name = args[0];
^
Greater.java:8: error: cannot find symbol
var name = args[0];
^
symbol: class var
location: class HelloWorld
1 error
1 warning
error: 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 RediVidEr
The string {RediVidEr} is a Palindrome!! True
使用“RaceCar”代替“RediVidEr”後,再運行一次:
mohamed_taman:code$ java PalindromeChecker.java RaceCar
The string {RaceCar} is a Palindrome!! True
最後,再使用“Taman”來代替“RaceCar”:
mohamed_taman:code$ java PalindromeChecker.java Taman
The 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.java
Response code is: 200
The 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中,有兩種基本的腳本編寫方法:
- 直接使用 java 命令工具。
- 使用 *nix 命令行腳本,它類似於 bash腳本
我們已經探討過了第一種方法了,所以現在是時候看一下第二種方法,這是一個可以打開許多可能性大門的特性。
Shebang文件:以shell腳本的形式運行Java
如前所述,Java SE 11 引入了對腳本的支持,包括支持傳統的*nix,即所謂的Shebang 文件。無需修改JLS(Java Language Specification,Java語言規範)就可以支持該特性。
在一般的Shebang文件中,前兩個字節必須是 0x23 和 0x21 ,這是"#!"兩個字符的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 11
import 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$ ./dirlist
Will 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
最後,我們通過展示一些使用該特性時需要注意的用法和技巧來結束本文。
用法和技巧
- 可以傳遞給 javac 的一些選項可能不會被Java工具所傳遞(或識別),比如, -processor和 -Werror 選項。
- 如果類路徑中同時存在.class和.java文件,啓動程序將強制使用字節碼類文件。
mohamed_taman:code$ javac HelloUniverse.java
mohamed_taman:code$ java HelloUniverse.java
error: 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 命令行執行腳本成爲可能。
今天就開始嘗試使用這個新特性吧,祝大家編程愉快。如果喜歡這篇文章,請將它分享給更多的極客。
參考資源
- JEP 330: Launch Single-File Source-Code Programs
- JEP 12: Preview Language and VM Features
- JDK 11 Documentation
- Java SE Development Kit 12.0.1
- Test your front-end against a real API
作者介紹
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/