參考資料##
該文中的內容來源於 Oracle 的官方文檔。Oracle 在 Java 方面的文檔是非常完善的。對 Java 8 感興趣的朋友,可以直接找到這個總入口 Java SE 8 Documentation ,想閱讀什么就點什么。本博客不定期從 Oracle 官網搬磚。這里介紹的工具是 jar 和 jarsigner 。
前言##
在前面的 在Linux中安裝Oracle JDK 8以及JVM的類加載機制 這一篇中我已經初步討論過 Java 程序的組成:Java 程序中沒有獨立函數,只有類和類中的方法,即使是程序的入口點也不是獨立函數。 Java 程序的源代碼存在於名為 *.java
的源代碼文件中,然后經過 javac
命令進行編譯,最終可生成名為 *.class
的類文件。 Java 程序的啟動器是 java
命令,它負責加載相應的類並執行其中的指令。
Java 程序的這種組織方式和我們常用的文件系統契合度非常好,一個類就是一個文件,類名就是文件名。(當然也有例外,比如內部類,這里不做討論。)更進一步,Java 中還有 package 的概念,而且一個 package 名(類似於 abc.def.ghi.*.class
)正好對應文件系統的路徑(類似於 abc/def/ghi/*.class
)。這種對應關系不是可有可無的,而是強制性的,我們在組織源代碼和類的時候,必須遵守這個准則,否則程序將無法運行。文件多了,自然需要將其打包成一個整體,這就需要用到 jar
命令生成文件名為 *.jar
的 jar 包文件,該 jar 包文件就是一個很常見的壓縮包文件,它其中的內容完全維持文件系統中的那種樹狀結構,隨時可以解包查看其中的文件。將庫或程序打包成 jar 文件進行發布已經是 Java 世界的標准做法,為了安全, jar 文件還可以被簽名和驗證。這正是 Java 世界的方便所在。
Java 程序中的 package 名和類文件的路徑的對應關系##
這里寫一個 HelloWorld 程序來做示范。本來一個 HelloWorld 程序是可以很簡單的,在 Java 中只需要一個 System.out.println("Hello, World!");
即可。但是為了讓類多一點,我把它寫得稍微復雜了點。我先寫了一個 Speaker
類,然后在 HelloWorld
類的 main
方法中調用 speaker.sayHello();
方法來和這個世界打招呼。同時,我的兩個類都定義在一個 package 中,如下圖源代碼中的 package com.xkland.sample;
:
前面講過, package 名必須和文件的路徑一一對應,所以,我將源文件放在了 src
目錄的 com/xkland/sample
目錄中,其實這不是必須的,源文件可以隨便放,只是這么放是一個好習慣。但是類文件所在的路徑就必須和 package 名完全一致了,否則程序無法執行。如下圖,我使用 javac src/com/xkland/sample/*.java -d dst
命令編譯源文件,使用 -d dst
選項就是讓 javac
把生成的類文件放到 dst
目錄中,而 javac
在 dst
目錄中自動生成了和 package 名完全一致的目錄樹。
然后,我們執行程序的時候,必須在 dst
目錄中運行 java com.xkland.sample.HelloWorld
,在其它的目錄中運行都不行,即使在 dst/com/xkland/sample
目錄下也不行,哪怕這里是 HelloWorld.class
所在的位置。(其實想在 dst
目錄以外的地方運行該程序也有辦法,那就是把 dst
目錄加入到 CLASSPATH 中,這里不做討論。)這個例子雖簡單,但充分展示了 Java 中的 package
和 類文件在文件系統中的路徑
之間必須遵守的約定。將類文件打包也必須遵守這樣的約定。
使用 jar 命令將程序打包##
讓類就這樣分散在文件系統中畢竟不是最方便的,前面講過可以把類文件打包,生成一個 jar 文件,這里來進行實戰。(其實打包的文件中可以包含任意類型的文件,不僅只是 Java 的類文件,圖片視頻什么的都可以,文本文件自然不在話下,這些東西都是資源,這里也不做深入討論。) jar 包還可以作為一個單獨的程序運行,使用 java -jar filename.jar
命令即可。由於每一個 jar 包中包含了不止一個類文件,所以要作為單獨的程序運行,在生成 jar 包的時候就必須指定程序的入口點,這個可以通過 jar
命令的 -e
參數指定。
使用 jar
命令打包的時候最重要的注意事項也是前面提到的 package 名和類文件的路徑的對應。先看下圖:
在該圖中,我使用了 jar
命令的 -cfe
選項,其中的 c
是創建 jar 文件,f
是指定 jar 文件的文件名,e
是指定程序的入口點。前面提到過,一定要注意 package 名和類文件的路徑的對應關系,所以在這個例子中,使用 jar
命令打包時,要么先進入 dst
目錄,再運行 jar -cfe HelloWorld.jar com.xkland.sample.HelloWorld com
,要么使用 jar
命令的 -C
指定 jar 包中的類文件的路徑從哪個目錄開始。我這里用的就是 jar -cfe HelloWorld.jar com.xkland.sample.HelloWorld -C dst com
。這里的 com
目錄會自動全部打包進 jar 文件,包括其中的所有子目錄和文件,也就是說,對於目錄的打包是遞歸的。而且,運行下面這樣的命令效果應該是一樣的: jar -cfe HelloWorld.jar com.xkland.sample.HelloWorld -C dst .
,這里的 .
代表當前目錄,也就是 dst
目錄中的所有東西都會進行打包;
像上面這樣打包后,使用 java -jar HelloWorld.jar
可以運行程序,使用 jar -tf HelloWorld.jar
可以查看 HelloWorld.jar
中的內容。可以看到,類文件的路徑為 com/xkland/sample/HelloWorld.class
和 com/xkland/sample/Speaker.class
,正好和 package 名完全對應。還可以看到 HelloWorld.jar
中有一個 META-INF/MANIFEST.MF
文件,這個文件是 jar 包文件的靈魂,所有的配置信息都在這里,比如程序的入口點是什么就是保存在這里,它是一個純文本文件,可以直接讀寫,但是我們實際工作中基本不需要自己手動編寫該文件,所以這里不做深入討論。
如果使用 jar
打包的時候沒有選擇正確的開始目錄,則 jar 包中類文件的路徑就會不正確,程序就無法運行。如下圖,打包時既沒有進入 dst
目錄,又沒有使用 -C dst
選項,結果打包后程序就無法正確運行了。使用 jar -tf HelloWorld.jar
查看一下,發現所有類文件的路徑都不對,因此程序無法運行。
關於 jar 的更多內容,可以直接查看 jar命令的手冊,或者查看 Java教程中關於jar的章節 。
jar 包的簽名和驗證##
在我介紹 JDK中的證書生成和管理工具keytool 時,已經簡單的講過網絡安全、證書、簽名等方面的內容,這里只需要實戰一下即可。現在已經有了一個 HelloWorld.jar
文件,嘗試一下使用 jarsigner
命令對它進行簽名。簽名之前,先得有個證書,所以先使用 keytool -genkeypair -alias youxia
為自己創建一個,別名為 youxia
。然后,使用這個證書對 HelloWorld.jar
進行簽名,命令為 jarsigner HelloWorld.jar youxia
。最后,可以使用 jarsigner -verify HelloWorld.jar
對 HelloWorld.jar
進行驗證。如下圖:
不動手不知道,一動手才發現 So Easy!更多的細節可以戳 Signing JAR Files 和 Verifying Signed JAR Files。
jar 文件被簽名后,里面多了一些文件,把它解包看一下,如下圖:
可以看到:首先是 MANIFEST.MF
文件中多了幾行,它為 jar 包中的每一個文件都生成了一個數據摘要,這個摘要是從 jar 包中包含的文件本身計算出來的;其次,多了一個 YOUXIA.SF
文件,其中的內容也是 jar 包中每個文件對應的摘要,但是這個摘要是從 MANIFEST.MF
中的數據項計算出來的,它同時包含有針對整個 MANIFEST.MF
文件計算出的摘要;最后,就是一個無法直接閱讀的文件 YOUXIA.DSA
,從圖中可以看出這個文件顯示為亂碼,其中的內容就是 youxia 的公鑰以及使用 youxia 的私鑰對該 jar 文件進行簽名后的結果。具體信息請看 Understanding Signing and Verification。
有了這里的直觀的印象,我們就對簽名和驗證有了更深入的了解。簽名和驗證是建立在信息摘要算法和非對稱加密解密算法的基礎上的。數據摘要算法是不可逆的,它只能從數據生成摘要,不能從摘要解密出數據。非對稱加密解密算法需要公鑰私鑰對,使用私鑰加密的數據只能使用公鑰解密,因此使用私鑰對上一步生成的摘要進行加密,就相當於是簽名了,因為只能通過相應的公鑰進行解密。一般情況下公鑰是通過證書發布出去的,而在上面的例子中,簽名者的公鑰直接放在了 YOUXIA.DSA
文件中,方便驗證者使用。
總結##
jar
和 jarsigner
這兩個命令用起來沒有什么難度,主要是理解其中的思想。使用 jar
時,一定要注意 package 名和類文件的路徑之間的對應關系;使用 jarsigner
時,要理解公鑰私鑰、證書、摘要和數字簽名,而且 JDK 中提供了非常好用的生成和管理公鑰私鑰及證書的工具 keytool
。對於這些工具,我們只要親自動手試一下,就可以加深我們對 Java 安全方面的理解。至於這些命令的細節都不需要多記,用的時候查官方文檔即可。