Tìm hiểu về khái niệm Classpath trong Java. Hướng dẫn cách biên dịch code Java thành bytecode và chạy bytecode thông qua command line.
1. Sơ lược & chuẩn bị
1.1. javac
và java
command
Bạn nào học Java cũng biết, để chạy được chương trình Java cần hai bước biên dịch thành bytecode và thực thi bytecode đó. Với mỗi giai đoạn sẽ dùng command tương ứng là javac
và java
như sau.
# Biên dịch Main.java thành Main.class
javac Main.java # Chạy bytecode trong Main.class
java Main
Chương trình đơn giản như Hello World sẽ biên dịch bình thường mà không cần cấu hình classpath. Tuy nhiên, nếu chương trình có import qua lại với nhau, thì nên biết thêm về classpath.
1.2. Code ví dụ
Để test được classpath, mình sẽ tạo chương trình demo gồm 3 file như sau.
import toyota.Car; public class Main { public static void main(String[] args) { System.out.println("Bike: " + Bike.WHEEL_COUNT); System.out.println("Car: " + Car.WHEEL_COUNT); }
}
Và file code thứ hai Bike.java
nằm cùng thư mục với Main.java
.
public class Bike { public static int WHEEL_COUNT = 2;
}
File code thứ ba nằm trong một thư mục con toyota
(test package hoạt động ra sao khi custom classpath).
package toyota; public class Car { public static int WHEEL_COUNT = 4;
}
Việc biên dịch và chạy chương trình không có lỗi gì cả. Quá trình biên dịch tạo ra 3 file class tương ứng nằm cùng vị trí với file code.
Biên dịch Main.java
cho ra 3 file .class
, nằm cùng vị trí với file code tương ứng. Các class liên quan, được sử dụng trong Main.java
cũng được biên dịch theo, ngược lại những class không dùng đến thì không.
Dùng flag -d
khi biên dịch sẽ cho ra các file class nằm trong một thư mục riêng (nhưng vẫn giữ cấu trúc như các file code).
javac -d ./target Main.java
Cũng có thể dùng dấu wildcard * để biên dịch toàn bộ file trong thư mục nào đó (các thư mục con thì không).
javac ./toyota/*.java
2. Khái niệm Classpath
2.1. Classpath khi compile
Chương trình ở phần trên chạy không có lỗi, do mặc định classpath được đặt là thư mục hiện tại. Vô tình classpath này lại khớp với các package và chương trình chạy được.
Tuy nhiên, hãy thay đổi một chút bằng cách di chuyển ra ngoài thư mục gốc (thư mục chứa file Main.java
) và compile lại. Đã có lỗi xuất hiện như hình sau.
Java Compiler không thể tìm thấy các class liên quan (Bike
và Car
) khi biên dịch Main
class. Đây là do sự không phù hợp giữa khai báo package và classpath.
Khi biên dịch Java Compiler sẽ tìm kiếm các class khác liên quan. Class cần tìm sẽ bao gồm package (dựa theo import) và tên class, ví dụ
toyota.Car
.Compiler dựa vào classpath, package và tên class tìm được file
.java
tương ứng. Ví dụ classpath đang ở.
(mặc định), class làtoyota.Car
thì file Java sẽ nằm tại./toyota/Car.java
.Compiler sẽ phân tích file Java và tìm class cần tìm, nếu có thì biên dịch thành file
.class
mới, ngược lại báo lỗi. Lỗi này có thể do class khai báo sai package nên tên class không khớp. Ví dụ cần tìmtoyota.Car
mà trong file Java lại khai báo làyamaha.Car
.
Quay lại trên, do classpath chưa đúng nên Java Compiler không thể tìm được các file .java
liên quan để biên dịch. Vì vậy, cần chỉ định classpath khi biên dịch bằng flag -cp
hoặc -classpath
như sau.
javac -cp ./test-classpath ./test-classpath/Main.java
Ví dụ tìm class Bike
, lúc trước classpath là .
, nối với package của class Bike
là rỗng (default package), nên file Bike.java
sẽ nằm ở ./Bike.java
. Nhưng đường dẫn không khớp nên bị lỗi.
Nhưng khi chỉnh lại classpath như trên thành ./test-classpath
, nên file Bike.java
nằm tại ./test-classpath/Bike.java
. Java Compiler tìm được và biên dịch bình thường.
Tóm lại, classpath chỉ là một đường dẫn để javac
và java
tìm được đâu là gốc của package, dựa vào đó để tìm các file .java
khác thôi.
2.2. Classpath ở runtime
Đúng ra thì khi biên dịch với javac
phải dùng -sourcepath
mới đúng. Cơ bản thì sourcepath với classpath chỉ khác nhau là một cái dùng khi compile, một cái dùng khi chạy bytecode. Mà với classpath thì dùng được ở cả 2 luôn, nên mình prefer hơn.
Như phần trên, command java
được dùng để thực thi bytecode đã biên dịch. Lúc này cũng cần chú ý đến classpath nếu không muốn bị ClassNotFoundException
.
Ở đây mình biên dịch vào thư mục ./target
, nên classpath sẽ là thư mục này.
# Biên dịch trước
javac -d ./target -cp ./test-classpath ./test-classpath/Main.java # Chạy class Main
java -cp ./target Main
Phần này khác một tí, chúng ta sẽ chỉ định tên class cần chạy gồm package và class name như sau. Tất nhiên class phải có public static void main
thì mới chạy được.
3. Các khía cạnh khác
3.1. Chỉ định nhiều classpath
Sẽ có trường hợp bạn cần chỉ định nhiều classpath cùng lúc, ví dụ như các file Java có cùng package nhưng khác thư mục. Trong trường hợp đó, flag -cp
cần chỉ định nhiều thư mục như sau:
- Trên Windows dùng dấu
;
để phân tách - Trên Linux dùng dấu
:
để phân tách
Tiến hành sửa lại code như sau để kiểm tra.
// Bỏ dòng khai báo package
// Car sẽ cùng default package với Main (nhưng khác thư mục)
// package toyota; public class Car { public static int WHEEL_COUNT = 4;
}
// Do cùng default package nên không cần import
// import toyota.Car; public class Main { public static void main(String[] args) { System.out.println("Bike: " + Bike.WHEEL_COUNT); System.out.println("Car: " + Car.WHEEL_COUNT); }
}
Biên dịch và chạy chương trình đã sửa. Chương trình vẫn hoạt động bình thường.
# Biên dịch
javac -cp "./test-classpath:./test-classpath/toyota" ./test-classpath/Main.java # Chạy bytecode
java -cp "./test-classpath:./test-classpath/toyota" Main
Trường hợp này Java Compiler sẽ tìm class trong tất cả classpath. Và file Car.java
dù có khai báo package không cũng cho kết quả tương tự, vì compiler tìm ở hai classpath khác nhau (nhưng khi biên dịch ra thư mục riêng thì cấu trúc file .class
sẽ khác).
Ngoài ra, nhiều classpath cũng hỗ trợ trong việc chạy bytecode với các file JAR ở phần tiếp theo.
3.2. Classpath với file JAR
Hầu như các thư viện Java hiện tại được đóng gói dưới dạng file JAR (Java Archive). Nhưng nếu muốn biên dịch và chạy chương trình gồm file JAR qua command line thì làm thế nào?
Ở đây mình dùng thư viện Apache Common Lang để demo. Chỉ cần download về, giải nén ra và copy file JAR vào thư mục nào đó là được.
https://commons.apache.org/proper/commons-lang/download_lang.cgi
File JAR trong trường hợp này có thể xem như một thư mục chứa các file .class
(thử dùng WinRAR mở ra thử). Như trong hình, việc sử dụng file JAR giống như chỉ định thêm một classpath.
Cần chú ý ở đây là khi biên dịch, Java Compiler nếu tìm thấy file .class
rồi thì không cần biên dịch file .java
nữa. Thêm nữa, khi chỉ định classpath là JAR thì có thể dùng wildcard để load tất cả file .jar
trong thư mục nào đó.