1. 程式人生 > 程式設計 >第1期:拋開IDE,瞭解一下javac如何編譯

第1期:拋開IDE,瞭解一下javac如何編譯

IDE或maven等工具已將Java程式的編譯代勞。但工具越高階,隱藏的細節就越多,一旦出現問題就懵逼,歸根到底還是基礎概念不牢靠。返璞歸真,回到最原始的地方javac,會讓問題豁然開朗。下面就一步一步演示用javacjava徒手編譯執行一個常規工程。

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!

上面的步驟,說明瞭兩點:

  1. 增加了package名,所以class名也變了,行不改名坐不改姓,自然要帶上姓(即所謂全限定名)。
  2. 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.javaHelloWorld.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檔案或其他資源的路徑,javacjava等工具都可以指定類路徑。如果沒有設定,預設的類路徑就是當前目錄。但如果設定了類路徑,預設值就被覆蓋了,所以如果想保留當前目錄為類路徑,需要同時將.加入,有點像預設建構函式的感覺。

類路徑,可以通過環境變數CLASSPATH-cp引數設定,後者會覆蓋前者。推薦通過-cp設定,它只會影響當前程式。

類路徑類似作業系統裡的path概念,不過它是java工具搜尋class檔案的路徑。同樣的,類路徑可以是多個,並通過分號分隔:

export CLASSPATH=path1:path2:...
複製程式碼

或者:

sdkTool -classpath path1:path2:...
複製程式碼

sdkTool可以是 java,javac,javadoc等。

類路徑不僅可以是目錄,還也可以是jar包或zip包。

類路徑的設定是有順序的,java會優先在靠前的類路徑裡搜尋。這一點和作業系統的path類似。

類路徑可以用萬用字元*匹配jar或zip,但

  1. 萬用字元只匹配jar或zip,比如path/*只是將下面的jar或zip加入類路徑,但path本身不加入類路徑。
  2. 萬用字元不遞迴搜尋,即指匹配第一層目錄下的jar或zip。
  3. 萬用字元匹配到的jar或zip,加入到classpath的順序是不確定的。因此,更穩妥的做法是顯示的列舉所有jar或zip。
  4. 萬用字元適用於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的基本用法總結如下:

  1. -cp引數設定類路徑,基本用法是將編譯時依賴的jar包加入類路徑。並可用*通配jar包。
  2. -d 引數用來設定class檔案編譯到單獨目錄,並根據包名建立子目錄。
  3. 理論上將java檔案的路徑全部傳給javac即可,但操作上,可以通過find命令將檔案列表輸出到檔案中,通過@argfiles引數傳遞。

參考

  1. docs.oracle.com/javase/8/do…
  2. docs.oracle.com/javase/8/do…
  3. docs.oracle.com/javase/8/do…
  4. docs.oracle.com/javase/tuto…
  5. docs.oracle.com/javase/8/do…

github.com/maoshuai/ja…


《Java與Linux學習週刊》每週五發布,同步更新於:Github知乎掘金