第1期:拋開IDE,瞭解一下javac如何編譯
IDE或maven等工具已將Java程式的編譯代勞。但工具越高階,隱藏的細節就越多,一旦出現問題就懵逼,歸根到底還是基礎概念不牢靠。返璞歸真,回到最原始的地方javac
,會讓問題豁然開朗。下面就一步一步演示用javac
和java
徒手編譯執行一個常規工程。
Hello World練個手
來個簡單的先,我們祭出祖傳的HelloWorld程式。(感興趣的話,可以試一試徒手是否寫的出來~)
public class HelloWorld{
public static void main(String[] args){
System.out.println("Hello,World!");
}
}
複製程式碼
寫完後,儲存為:HelloWorld.java
,然後在當前目錄執行javac
編譯命令:
javac HelloWorld.java
複製程式碼
檢視當前目錄(更準確的說是java檔案同級目錄),果然生成了HelloWorld.class
:
maoshuai@ms:~/javaLinux/w1$ ls
HelloWorld.class HelloWorld.java
複製程式碼
繼續在當前目錄執行java
命令,正確打印出Hello,World!
maoshuai@ms:~/javaLinux/w1$ java HelloWorld
Hello,World!
複製程式碼
老司機,穩!看起來很簡單嘛:先javac
java
。
雖然簡單,但新手通常會犯的一個錯:想象成去執行.class
檔案,比如寫成這樣,自然會報錯:
maoshuai@ms:~/javaLinux/w1$ java HelloWorld.class
Error: Could not find or load main class HelloWorld.class
複製程式碼
需要明白,java
的引數,傳入的是main函式所在的類的名字,而不是class檔案;java會根據類名自動去找class檔案。
帶個包名
一切都很順利,但沒有包名是不專業的,所以我們加一個牛逼的包com.imshuai.javalinux
:
package com.imshuai.javalinux;
public class HelloWorld{
public static void main(String[] args){
System.out.println("Hello,World!");
}
}
複製程式碼
還是一樣用javac
編譯,檢視當前目錄下HelloWorld.class
生成了,很順利。
還是一樣用java
命令,瞬間被打臉:
maoshuai@ms:~/javaLinux/w1$ java HelloWorld
Error: Could not find or load main class HelloWorld
複製程式碼
想了想,HelloWorld已經有自己的包名了,所以它的名字不在是沒有姓氏的HelloWorld
,新名字叫com.imshuai.javalinux.HelloWorld
,那麼傳給java
自然要用新名字,再試一試:
maoshuai@ms:~/javaLinux/w1$ java com.imshuai.javalinux.HelloWorld
Error: Could not find or load main class com.imshuai.javalinux.HelloWorld
複製程式碼
還是被打臉,這時候老司機告訴你,建立一個com/imshuai/javalinux
目錄,然後把HelloWorld.class
放進來,執行:
maoshuai@ms:~/javaLinux/w1$ mkdir -p com/imshuai/javalinux
maoshuai@ms:~/javaLinux/w1$ mv HelloWorld.class com/imshuai/javalinux
maoshuai@ms:~/javaLinux/w1$ java com.imshuai.javalinux.HelloWorld
Hello,World!
複製程式碼
果然,正常打印出了Hello,World!
上面的步驟,說明瞭兩點:
- 增加了package名,所以class名也變了,行不改名坐不改姓,自然要帶上姓(即所謂全限定名)。
- Java 會根據包名對應出目錄結構,並從class path搜尋該目錄去找class檔案。由於預設的class path是當前目錄,所以
com.imshuai.javalinux.HelloWorld
必須儲存在./com/imshuai/javalinux/HelloWorld.class
當然每次自己建立包路徑的目錄太麻煩。-d
引數可以代勞上面的工作:
maoshuai@ms:~/javaLinux/w1$ javac -d . HelloWorld.java
maoshuai@ms:~/javaLinux/w1$ ls
com HelloWorld.java
maoshuai@ms:~/javaLinux/w1$ java com.imshuai.javalinux.HelloWorld
Hello,World!
複製程式碼
-d
指定了生成class檔案的根目錄(這裡用的是當前目錄),並且會根據class的包路徑建立子目錄。
編譯兩個有依賴關係的class
包名解決了,我們再複雜些,搞個依賴呼叫。首先,我們抽取一個HelloService
:
package com.imshuai.javalinux;
public class HelloService{
public void printHello(String name){
System.out.println("Hello," + name + "!");
}
}
複製程式碼
然後修改HelloWorld.java
,呼叫HelloService
完成say hello:
package com.imshuai.javalinux;
public class HelloWorld{
public static void main(String[] args){
HelloService service = new HelloService();
service.printHello("World");
}
}
複製程式碼
接著我們依次編譯:HelloService.java
和HelloWorld.java
,最後執行:
maoshuai@ms:~/javaLinux/w1$ javac -d . HelloService.java
maoshuai@ms:~/javaLinux/w1$ javac -d . HelloWorld.java
maoshuai@ms:~/javaLinux/w1$ ls
com HelloService.java HelloWorld.java
maoshuai@ms:~/javaLinux/w1$ java com.imshuai.javalinux.HelloWorld
Hello,World!
複製程式碼
直覺上,要先編譯HelloService.java
,這是對的。那如果先編譯HelloWorld.java
呢?當然是打臉:
maoshuai@ms:~/javaLinux/w1$ javac -d . HelloWorld.java
HelloWorld.java:4: error: cannot find symbol
HelloService service = new HelloService();
^
symbol: class HelloService
location: class HelloWorld
HelloWorld.java:4: error: cannot find symbol
HelloService service = new HelloService();
^
symbol: class HelloService
location: class HelloWorld
2 errors
複製程式碼
如果編譯的時候,還要根據依賴關係確定順序,太low了吧。我覺得java
命令應該能自動解決它,一次性將兩個java檔案傳給它試一試:
maoshuai@ms:~/javaLinux/w1$ javac -d . HelloWorld.java HelloService.java
maoshuai@ms:~/javaLinux/w1$ ls
com HelloService.java HelloWorld.java
maoshuai@ms:~/javaLinux/w1$ java com.imshuai.javalinux.HelloWorld
Hello,World!
複製程式碼
牛逼,它自動解決了順序問題,贊一個(雖然我不懷好意的將HelloWorld.java
放到了前面)!
使用src和target目錄
從上面的例子可以看出,雖然class檔案必須放在包名一致的目錄裡,但java原始檔並沒有這個要求。不過,為了管理方便,我們將java原始檔也放在包結構目錄裡:
maoshuai@ms:~/javaLinux/w1$ mkdir -p com/imshuai/javalinux
maoshuai@ms:~/javaLinux/w1$ mv *.java com/imshuai/javalinux/
maoshuai@ms:~/javaLinux/w1$ ls com/imshuai/javalinux/
HelloService.java HelloWorld.java
maoshuai@ms:~/javaLinux/w1$ javac -d . com/imshuai/javalinux/*.java
maoshuai@ms:~/javaLinux/w1$ ls com/imshuai/javalinux/
HelloService.class HelloService.java HelloWorld.class HelloWorld.java
maoshuai@ms:~/javaLinux/w1$ java com.imshuai.javalinux.HelloWorld
Hello,World!
複製程式碼
編譯時javac
要傳入新的java檔案路徑(這裡用了萬用字元),其他也沒有什麼不同。可以看到class檔案生成到了與java檔案相同的目錄裡。class檔案和java原始檔放在一起,很不清爽,能否像IDE裡那樣:java檔案放到src目錄,class檔案放到target目錄?下面我試一試。
先建立src和target目錄,並將原來的java檔案都移動到src目錄:
maoshuai@ms:~/javaLinux/w1$ mkdir src
maoshuai@ms:~/javaLinux/w1$ mkdir target
maoshuai@ms:~/javaLinux/w1$ mv com src
maoshuai@ms:~/javaLinux/w1$ ls
src target
複製程式碼
然後編譯,-d
引數指定到target目錄:
maoshuai@ms:~/javaLinux/w1$ javac -d target src/com/imshuai/javalinux/*.java
maoshuai@ms:~/javaLinux/w1$ ls target/com/imshuai/javalinux/
HelloService.class HelloWorld.class
複製程式碼
怎麼執行呢?直接在當前目錄執行是不行了,畢竟多了一層target目錄,進入target目錄執行,妥妥的:
maoshuai@ms:~/javaLinux/w1/target$ java com.imshuai.javalinux.HelloWorld
Hello,World!
複製程式碼
除了進入target
目錄以外,更常用的方法是通過-classpath
(或簡寫為-cp
)選項設定類路徑:
maoshuai@ms:~/javaLinux/w1$ java -cp target com.imshuai.javalinux.HelloWorld
Hello,World!
複製程式碼
類路徑CLASSPATH
上面演示了通過-cp
設定類路徑。下面再進一步研究一下類路徑。
類路徑,是JRE搜尋使用者級class檔案或其他資源的路徑,javac
或java
等工具都可以指定類路徑。如果沒有設定,預設的類路徑就是當前目錄。但如果設定了類路徑,預設值就被覆蓋了,所以如果想保留當前目錄為類路徑,需要同時將.
加入,有點像預設建構函式的感覺。
類路徑,可以通過環境變數CLASSPATH
或-cp
引數設定,後者會覆蓋前者。推薦通過-cp
設定,它只會影響當前程式。
類路徑類似作業系統裡的path
概念,不過它是java工具搜尋class檔案的路徑。同樣的,類路徑可以是多個,並通過分號分隔:
export CLASSPATH=path1:path2:...
複製程式碼
或者:
sdkTool -classpath path1:path2:...
複製程式碼
sdkTool可以是 java,javac,javadoc等。
類路徑不僅可以是目錄,還也可以是jar包或zip包。
類路徑的設定是有順序的,java會優先在靠前的類路徑裡搜尋。這一點和作業系統的path
類似。
類路徑可以用萬用字元*
匹配jar或zip,但
- 萬用字元只匹配jar或zip,比如path/*只是將下面的jar或zip加入類路徑,但path本身不加入類路徑。
- 萬用字元不遞迴搜尋,即指匹配第一層目錄下的jar或zip。
- 萬用字元匹配到的jar或zip,加入到classpath的順序是不確定的。因此,更穩妥的做法是顯示的列舉所有jar或zip。
- 萬用字元適用於
CLASSPATH
變數或-cp
引數,但不適用於jar包的manifest檔案。
更真實的場景
下面的java專案有較多的包結構,還有jar包依賴
,如何編譯呢?
(工程程式碼下載:github.com/maoshuai/ja…)
├── lib
│ ├── logback-classic-1.2.3.jar
│ ├── logback-core-1.2.3.jar
│ └── slf4j-api-1.7.26.jar
├── resources
│ └── logback.xml
├── src
│ └── com
│ └── imshuai
│ └── javalinux
│ ├── HelloWorld.java
│ └── service
│ ├── IGreetingService.java
│ └── impl
│ ├── AlienGreetingService.java
│ ├── CatGreetingService.java
│ ├── DogGreetingService.java
│ └── HumanGreetingService.java
└── target
複製程式碼
最直接的辦法,跟剛才一樣,只不過體力活多一些,要列舉所有的java檔案,同時使用萬用字元將lib下的jar加入類路徑:
javac \
-cp "lib/*" \
-d target \
src/com/imshuai/javalinux/HelloWorld.java \
src/com/imshuai/javalinux/service/IGreetingService.java \
src/com/imshuai/javalinux/service/impl/*.java
複製程式碼
編譯成功,java
命令執行一下(注意:target和lib下的jar都需要加入類路徑):
maoshuai@ms:~/javaLinux/w1$ java -cp "target:lib/*" com.imshuai.javalinux.HelloWorld XiaoMing
22:16:15.887 [main] INFO HumanGreetingService - XiaoMing is saying hello: Ni Chou Sha!
複製程式碼
如果檔案很多,手工列出這些檔案不太現實,可以通過find
命令完成:
javac -cp "lib/*" -d target $(find src -name "*.java")
複製程式碼
javac
還提供了一種列表檔案的辦法,即將要編譯的java檔案列表寫到一個文字檔案裡,我們用find命令完成:
maoshuai@ms:~/javaLinux/w1$ find src -name "*.java" >javaFiles.txt
複製程式碼
生成的javaFiles.txt內容如下:
src/com/imshuai/javalinux/service/IGreetingService.java
src/com/imshuai/javalinux/service/impl/HumanGreetingService.java
src/com/imshuai/javalinux/service/impl/CatGreetingService.java
src/com/imshuai/javalinux/service/impl/DogGreetingService.java
src/com/imshuai/javalinux/service/impl/AlienGreetingService.java
src/com/imshuai/javalinux/HelloWorld.java
複製程式碼
然後用@
開頭的@javaFiles.txt
,代表傳給javac
的是列表檔名:
javac -cp "lib/*" -d target @javaFiles.txt
複製程式碼
不僅如此,引數也可以放入檔案(注意:-cp
不能放進去)。比如javaFiles.txt
中加入-d target
-d target
src/com/imshuai/javalinux/service/IGreetingService.java
src/com/imshuai/javalinux/service/impl/HumanGreetingService.java
src/com/imshuai/javalinux/service/impl/CatGreetingService.java
src/com/imshuai/javalinux/service/impl/DogGreetingService.java
src/com/imshuai/javalinux/service/impl/AlienGreetingService.java
src/com/imshuai/javalinux/HelloWorld.java
複製程式碼
這樣只用執行:
javac -cp "lib/*" @javaFiles.txt
複製程式碼
不過為了清晰,我們可以把引數-d target
單獨放到一個檔案javaOptions.txt
,然後傳兩個@檔案:
javac -cp "lib/*" @javaOptions.txt @javaFiles.txt
複製程式碼
使用列表檔案的好處是,規避了命令列引數長度的限制,並且可以在任何作業系統上執行。
有了上面的準備,我們可以寫出一個自動化編譯的指令碼了:
PROJECT_DIR=/home/maoshuai/javaLinux/w1
# clean target directory
rm -rf $PROJECT_DIR/target/*
# prepare arg files
find $PROJECT_DIR/src -name "*.java">$PROJECT_DIR/target/javaFiles.txt
echo "-d $PROJECT_DIR/target" >$PROJECT_DIR/target/javaOptions.txt
# compile
javac -cp "$PROJECT_DIR/lib/*" @$PROJECT_DIR/target/javaOptions.txt @$PROJECT_DIR/target/javaFiles.txt
# copy resources to target
cp -rf $PROJECT_DIR/resources/* $PROJECT_DIR/target
# clean temp files
rm -rf $PROJECT_DIR/target/javaOptions.txt $PROJECT_DIR/target/javaFiles.txt
複製程式碼
是時候仔細看一下javac了
Oracle的官方檔案介紹javac
如下:
Reads Java class and interface definitions and compiles them into bytecode and class files.
javac的語法如下:
javac [ options ] [ sourcefiles ] [ classes] [ @argfiles ]
複製程式碼
- options:是一些引數,比如-cp,-d
- sourcefiles:就是編譯的java檔案,如
HelloWorld.java
,可以是多個,並用空格隔開 - classes:用來處理處理註解。暫時沒搞懂怎麼用
- @argfiles,就是包含option或java檔案列表的檔案路徑,用@符號開頭,就像上面的@javaOptions.txt和@javaFiles.txt
總結
將javac
的基本用法總結如下:
-
-cp
引數設定類路徑,基本用法是將編譯時依賴的jar包加入類路徑。並可用*
通配jar包。 -
-d
引數用來設定class檔案編譯到單獨目錄,並根據包名建立子目錄。 - 理論上將java檔案的路徑全部傳給
javac
即可,但操作上,可以通過find命令將檔案列表輸出到檔案中,通過@argfiles引數傳遞。
參考
- docs.oracle.com/javase/8/do…
- docs.oracle.com/javase/8/do…
- docs.oracle.com/javase/8/do…
- docs.oracle.com/javase/tuto…
- docs.oracle.com/javase/8/do…