I/O, NIO và reflection: đọc file đối soát và hiểu framework
Tiếp đi: I/O và NIO trong Java.
Mỗi đêm cổng thanh toán gửi cho shop-backend một file CSV đối soát (vài trăm MB): mỗi dòng là một Payment. Mình cần đọc, so với DB, ghi file báo cáo chênh lệch. Đây là chỗ I/O xuất hiện thật trong backend.
1. Bài toán: đọc cả file vào RAM
String content = new String(new FileInputStream("settlement.csv").readAllBytes());
for (String line : content.split("\n")) { ... } // 500 MB file → OutOfMemoryError
// và FileInputStream không bao giờ được close
2. Vấn đề
- Đọc hết vào bộ nhớ: file lớn là OOM.
- Quên close: rò file descriptor.
- Đọc từng byte không buffer: mỗi lần là một system call, rất chậm.
3. Giải pháp: hiểu 2 thế hệ API
- java.io (blocking, stream):
InputStream/OutputStreamcho byte,Reader/Writercho ký tự. Đọc tuần tự, thread chờ tới khi có dữ liệu. Xếp lớp bằng Decorator:BufferedReader(InputStreamReader(FileInputStream)). - java.nio:
Channel+Buffer, có thể non-blocking, vàSelectorđể một thread theo dõi nhiều channel. KèmPath/Files(NIO.2, Java 7) là API file hiện đại nên dùng.
4. Code từng bước
Bước 1: Đọc file lớn theo dòng, lazy
public record SettlementLine(String paymentId, BigDecimal amount) {
static SettlementLine parse(String line) {
String[] p = line.split(",");
return new SettlementLine(p[0], new BigDecimal(p[1]));
}
}
public List<String> findMismatches(Path file) throws IOException {
try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) { // lazy, có buffer
return lines.skip(1) // bỏ header
.map(SettlementLine::parse)
.filter(l -> !paymentRepo.matches(l.paymentId(), l.amount()))
.map(SettlementLine::paymentId)
.toList();
} // tự close
}
Bước 2: Ghi báo cáo và thao tác file bằng Files
Path report = Path.of(reportDir, "mismatch-" + LocalDate.now() + ".csv");
Files.createDirectories(report.getParent());
Files.write(report, mismatches, StandardCharsets.UTF_8);
Files.move(file, archiveDir.resolve(file.getFileName()), StandardCopyOption.ATOMIC_MOVE);
try (Stream<Path> files = Files.list(inboxDir)) { // liệt kê file chờ xử lý
files.filter(p -> p.toString().endsWith(".csv")).forEach(this::process);
}
Bước 3: NIO channel + buffer (biết để hiểu)
try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.allocateDirect(64 * 1024);
while (ch.read(buf) != -1) {
buf.flip(); // chuyển từ chế độ ghi sang đọc
// xử lý buf ...
buf.clear(); // sẵn sàng ghi tiếp
}
}
// Copy file nhanh, zero-copy ở mức OS:
srcChannel.transferTo(0, srcChannel.size(), destChannel);
5. Cách hoạt động
Blocking I/O (1 thread / connection) NIO + Selector (1 thread / nhiều connection)
Thread-1 ── read() ⏳ chờ client A ┌── Channel A ─┐
Thread-2 ── read() ⏳ chờ client B │── Channel B ─┼──▶ Selector ◀── 1 thread
Thread-3 ── read() ⏳ chờ client C └── Channel C ─┘ select(): "A, C có dữ liệu"
10k connection = 10k thread chỉ xử lý channel đã sẵn sàng
Netty, Tomcat NIO connector và WebFlux xây trên mô hình Selector này. Chi tiết event loop xem Event loop & non-blocking I/O.
6. So sánh
| java.io | java.nio | |
|---|---|---|
| Đơn vị | Stream (byte/char tuần tự) | Channel + Buffer (khối) |
| Blocking | Luôn blocking | Socket có thể non-blocking |
| Một thread nhiều connection | Không | Có (Selector) |
| Đọc lùi / nhảy vị trí | Không | Có (position()) |
| Dùng trong app thường ngày | Reader/Writer có buffer | Files, Path |
7. Khi nào nên dùng / không nên dùng
- File trong ứng dụng: dùng
Files/Path, luôn chỉ định charset, luôn try-with-resources. - Không tự viết server bằng Selector; dùng Netty/WebFlux. Với Java 21, virtual thread giúp code blocking đơn giản vẫn scale.
- Không
readAllBytes/readAllLinesvới file có thể lớn.
@Service, @Autowired trên class mình thế nào? Đó là reflection?Reflection là gì? Mình chưa bao giờ gọi nó, nhưng nghe nói Spring, Hibernate, Jackson dùng suốt.
Đúng rồi, bạn dùng gián tiếp hằng ngày. Reflection là khả năng đọc và thao tác cấu trúc class lúc runtime: liệt kê field, method, annotation, gọi method, set field private, tạo object, khi lúc viết code không biết class cụ thể.
Class<?> clazz = Order.class; // hoặc Class.forName("com.shop.Order")
for (Field f : clazz.getDeclaredFields()) {
System.out.println(f.getName() + " : " + f.getType().getSimpleName());
}
Order o = (Order) clazz.getDeclaredConstructor(String.class).newInstance("ORD-1");
Method m = clazz.getMethod("markPaid");
m.invoke(o);
Field status = clazz.getDeclaredField("status");
status.setAccessible(true); // phá private
status.set(o, OrderStatus.SHIPPED);
Framework làm gì với nó:
Spring: quét class → thấy @Service → newInstance() → field/constructor cần gì → inject
Jackson: JSON {"id":"ORD-1"} → tìm setter/field/constructor "id" → set giá trị
Hibernate: ResultSet → tạo entity bằng constructor rỗng → set field theo @Column
JUnit: tìm method có @Test → invoke
Tự viết annotation rồi đọc bằng reflection (ví dụ đo thời gian method) đã có ở Tự viết annotation. Điều kiện để đọc được lúc runtime: annotation phải có @Retention(RetentionPolicy.RUNTIME).
Reflection có chậm không? Mình định dùng nó để copy field từ OrderEntity sang OrderDto cho tiện.
Bạn hiểu gần đúng, chỉ cần chỉnh một điểm: phần chậm chủ yếu là tra cứu (getDeclaredField, getMethod) và kiểm tra quyền truy cập, còn invoke sau khi JIT đã tối ưu thì không còn chênh nhiều. Framework vì vậy tra một lần lúc khởi động rồi cache Method/Field.
| Chi phí | Ảnh hưởng |
|---|---|
| Tốc độ | Lookup chậm; cần cache. Từ Java 18, reflection chạy trên MethodHandle, nhanh hơn trước. |
| Type-safety | Đổi tên field không báo lỗi compile, chỉ nổ lúc chạy |
| Encapsulation | setAccessible phá private; module system (Java 9+) có thể chặn |
| Native image | GraalVM cần khai báo trước các class dùng reflection |
Với copy Entity → DTO: viết tay, dùng record + static factory, hoặc MapStruct (sinh code lúc compile, không reflection, đổi tên field là compile báo lỗi). Reflection để dành cho code framework/hạ tầng, không cho nghiệp vụ.
implements Serializable giờ còn cần không?Entity cũ trong project đều implements Serializable. Giờ trả JSON hết rồi thì còn cần không? Record có dùng được không?
Phân biệt hai loại serialization:
- Java serialization (
Serializable,ObjectOutputStream): định dạng nhị phân riêng của Java. Còn gặp ở session HTTP lưu ra Redis, cache cũ, JPA khuyến nghị cho composite key (@EmbeddedId). Nhược điểm: lỗ hổng bảo mật khi deserialize dữ liệu không tin cậy, dễ vỡ khi đổi class (nên khai báoserialVersionUID), chỉ Java đọc được. - JSON (Jackson): dùng cho API, Kafka, cache Redis hiện đại. Không cần
Serializable.
public record OrderDto(String id, OrderStatus status, BigDecimal total) {} // Jackson 2.12+ hỗ trợ record
public record OrderKey(String orderId, int lineNo) implements Serializable {} // record cũng Serializable được
// Java serialization với record đi qua canonical constructor → validate trong constructor vẫn chạy
Kết luận: DTO và JSON thì không cần Serializable. Giữ nó ở composite key JPA hoặc khi thật sự dùng Java serialization. transient đánh dấu field bị bỏ qua khi serialize (Java serialization; Jackson thì dùng @JsonIgnore).
Phỏng vấn hỏi I/O vs NIO và reflection thì trả lời sao?
Câu 1: IO và NIO khác nhau gì?
🇻🇳 Tiếng Việt:
IO là stream, blocking, mỗi connection cần một thread chờ. NIO dùng channel và buffer, socket có thể non-blocking, và Selector cho một thread theo dõi nhiều channel, là nền của Netty và WebFlux. Với file trong app em dùng
Files/Pathcủa NIO.2, đọc file lớn bằngFiles.linestrong try-with-resources.
🇬🇧 English:
IO is stream-based and blocking, so each connection needs a waiting thread. NIO uses channels and buffers, sockets can be non-blocking, and a Selector lets one thread watch many channels; that's the basis of Netty and WebFlux. For files I use NIO.2
Files/Pathand stream large files withFiles.linesinside try-with-resources.
Câu 2: Reflection là gì, framework dùng thế nào, nhược điểm?
🇻🇳 Tiếng Việt:
Reflection cho phép đọc và gọi field, method, constructor, annotation lúc runtime. Spring dùng để quét annotation và inject, Jackson để map JSON, Hibernate để tạo entity. Nhược điểm: lookup chậm nên phải cache, mất type-safety, phá encapsulation, khó với native image. Em không dùng trong code nghiệp vụ.
🇬🇧 English:
Reflection lets code inspect and invoke fields, methods, constructors and annotations at runtime. Spring uses it to scan annotations and inject dependencies, Jackson to map JSON, Hibernate to instantiate entities. Downsides: lookups are slow so they must be cached, you lose type safety, it breaks encapsulation and complicates native images. I keep it out of business code.
Câu 3: Serializable dùng để làm gì?
🇻🇳 Tiếng Việt:
Đánh dấu class được Java serialization chuyển thành byte. Ngày nay API dùng JSON nên DTO không cần; vẫn gặp ở composite key JPA và session/cache cũ. Cần
serialVersionUIDvà không deserialize dữ liệu không tin cậy.
🇬🇧 English:
It marks a class as convertible to bytes by Java serialization. APIs use JSON today, so DTOs don't need it; it still shows up in JPA composite keys and older session or cache stores. Declare a
serialVersionUIDand never deserialize untrusted data.
Cách nhớ ngắn gọn
- IO: stream, blocking, 1 thread/connection. NIO: channel, buffer, selector.
- File:
Files+ charset + try-with-resources, đọc lazy. - Reflection = framework nhìn vào class lúc runtime; tra một lần, cache.
- Annotation cần
RUNTIMEretention mới đọc được. - JSON không cần Serializable.
Tự kiểm tra
Vì sao Files.lines phải đặt trong try-with-resources?
Stream giữ file mở; không close thì rò file descriptor.
buf.flip() làm gì?
Đặt limit = position hiện tại, position = 0, chuyển buffer từ chế độ ghi sang đọc.
Selector giải quyết vấn đề gì?
Một thread theo dõi nhiều channel, chỉ xử lý channel đã sẵn sàng, không cần một thread chờ mỗi connection.
Vì sao framework cache Method/Field?
Lookup bằng reflection tốn kém; tra một lần lúc khởi động rồi dùng lại.
Annotation không có @Retention(RUNTIME) thì sao?
Không đọc được bằng reflection lúc chạy (mặc định là CLASS).