1. module-info.java là gì và nó ở đâu?
Một mô-đun trong Java luôn bắt đầu với tệp module-info.java. Đây giống như “hộ chiếu” của mô-đun, nơi bạn khai báo tên, các gói được export và các phụ thuộc vào mô-đun khác.
Tìm ở đâu?
Tệp module-info.java phải nằm ở thư mục gốc của mã nguồn mô-đun của bạn. Ví dụ, nếu cấu trúc dự án như sau:
project-root/
└── src/
└── my.module.name/
├── module-info.java
└── com/
└── example/
└── api/
└── MyClass.java
Ở đây my.module.name là tên mô-đun (quy tắc đặt tên sẽ nói sau).
Không có tệp này thì mô-đun không được coi là mô-đun!
Nếu thiếu nó — đó chỉ là một dự án Java kiểu cũ, dù có rất nhiều thư mục và lớp.
2. Cú pháp module-info.java: các thành phần chính
Hãy xem ngay ví dụ đơn giản nhất — không đáng sợ đâu, thật đấy!
module my.module.name {
exports com.example.api;
requires java.sql;
}
Phân tích từng phần:
module my.module.name { ... }
Đây là khai báo mô-đun với tên my.module.name. Tên mô-đun là định danh duy nhất, thường trùng với gói gốc (ví dụ, com.example.app).
Một điều thú vị: nếu bạn đặt tên mô-đun là java.base, trình biên dịch sẽ không vui đâu. Đừng cố gắng thay thế các mô-đun chuẩn.
exports com.example.api;
Dòng này nói rằng: “Tôi, mô-đun, sẵn sàng chia sẻ mọi thứ nằm trong gói com.example.api”. Mọi thứ bên trong gói này và được đánh dấu public sẽ hiển thị với các mô-đun khác. Mọi thứ còn lại — chỉ dùng nội bộ.
requires java.sql;
Ở đây chúng ta khai báo rõ ràng với trình biên dịch: “Tôi cần mô-đun chuẩn java.sql để hoạt động.” Nếu không có điều này, trình biên dịch sẽ không cho phép dùng các lớp từ mô-đun đó.
Từ khóa bổ sung (để mở rộng hiểu biết)
- opens <package>; — mở gói cho reflection (ví dụ, cho các thư viện tuần tự hóa như Jackson).
- uses <service-interface>; — cho biết mô-đun sử dụng một service (interface).
- provides <service-interface> with <implementation-class>; — cho biết mô-đun cung cấp một triển khai cho service.
Trong bài này, chúng ta sẽ tập trung vào exports và requires — chúng cần thiết trong phần lớn các dự án học tập và dự án thực tế.
3. Ví dụ về module-info.java
Ví dụ 1. Mô-đun tối thiểu
module com.example.hello {
exports com.example.hello.api;
}
- Mô-đun này chỉ export gói com.example.hello.api.
- Mọi thứ nằm, chẳng hạn, trong com.example.hello.internal sẽ bị ẩn khỏi các mô-đun khác, ngay cả khi ở đó có các lớp public.
Ví dụ 2. Mô-đun có phụ thuộc
module com.example.dbclient {
exports com.example.db.api;
requires java.sql;
}
Chúng ta có thể dùng JDBC, nhưng chỉ vì đã khai báo phụ thuộc vào java.sql.
Ví dụ 3. Nhiều gói được export
module com.example.library {
exports com.example.library.api;
exports com.example.library.utils;
}
Bạn có thể export bao nhiêu gói tùy ý (nhưng đừng export tràn lan — đó là ý nghĩa của mô-đun!).
4. Giới hạn và quy tắc
Tên mô-đun
- Thường trùng với gói gốc (ví dụ, com.example.app).
- Không được trùng với tên các mô-đun chuẩn (java.base, java.sql, v.v.).
- Không được chứa khoảng trắng, ký tự đặc biệt, bắt đầu bằng chữ số, v.v.
- Khuyến nghị: dùng tên miền đảo ngược của tổ chức hoặc dự án để tránh xung đột.
Một mô-đun — một module-info.java
Trong một mô-đun chỉ được phép có một tệp như vậy. Nếu có hai — trình biên dịch sẽ gây cho bạn “vụ bê bối mô-đun”.
Một gói chỉ được export từ một mô-đun
Cùng một gói không thể được export từ hai mô-đun khác nhau. Giống như có hai hộ chiếu cho một tên — nhà chức trách sẽ không chấp thuận.
Các gói bên trong mô-đun
Chỉ có thể export những gói thực sự tồn tại trong cấu trúc mã nguồn của mô-đun. Export một gói không tồn tại sẽ dẫn đến lỗi biên dịch.
5. Thực hành: tạo module-info.java trong dự án
Giả sử chúng ta có một dự án đơn giản với nội dung sau:
project-root/
└── src/
└── com.example.greetings/
├── module-info.java
└── com/
└── example/
└── greetings/
├── api/
│ └── Greeter.java
└── internal/
└── SecretSauce.java
Bước 1. Tạo module-info.java
module com.example.greetings {
exports com.example.greetings.api;
}
Bước 2. Thử dùng lớp từ gói internal trong mô-đun khác
Giả sử chúng ta có mô-đun thứ hai com.example.app, muốn truy cập SecretSauce:
module com.example.app {
requires com.example.greetings;
}
import com.example.greetings.internal.SecretSauce; // LỖI!
Kết quả:
Trình biên dịch sẽ nói: “Gói com.example.greetings.internal không được mô-đun com.example.greetings export.” Dù lớp SecretSauce là public, nó vẫn không khả dụng với các mô-đun khác.
Đây chính là tính đóng gói (encapsulation) ở cấp độ mô-đun!
Bước 3. Thử không khai báo requires
Nếu trong com.example.app chúng ta không viết requires com.example.greetings;, mà lại cố dùng lớp từ com.example.greetings.api, trình biên dịch sẽ báo lỗi:
package com.example.greetings.api is not visible
6. Các lỗi thường gặp khi làm việc với module-info.java
Lỗi 1: Tên mô-đun không khớp với cấu trúc dự án.
Nếu bạn đặt tên mô-đun là com.example.app, nhưng cấu trúc thư mục lại là src/main/java/app, trình biên dịch sẽ không hiểu bạn muốn gì. Tên mô-đun thường trùng với gói gốc và các thư mục phải phản ánh điều đó.
Lỗi 2: Export tất cả mọi thứ.
Chỉ export những gì thực sự cần hiển thị cho mô-đun khác. Đừng làm exports com.example; chỉ vì “cho nhanh”. Điều đó phá vỡ tính đóng gói.
Lỗi 3: Quên thêm requires.
Nếu bạn dùng các lớp từ mô-đun khác hoặc thư viện chuẩn (ví dụ, java.sql) nhưng quên khai báo phụ thuộc — sẽ có lỗi biên dịch.
Lỗi 4: Gói không tồn tại trong exports.
Nếu bạn viết exports com.example.foo; nhưng gói đó không tồn tại — trình biên dịch sẽ nói rằng bạn đang “export không khí”.
Lỗi 5: Lớp public nằm trong gói không được export.
Nếu lớp được khai báo public nhưng nằm trong gói không được export, lớp này chỉ hiển thị bên trong mô-đun. Đây không phải là lỗi, nhưng thường gây bất ngờ cho người mới.
Lỗi 6: Nhiều module-info.java trong một mô-đun.
Trong một mô-đun chỉ nên có một tệp module-info.java. Nếu có hai — trình biên dịch sẽ không thể build dự án.
GO TO FULL VERSION