I/O, NIO và reflection: đọc file đối soát và hiểu framework

5 câu hỏi~30 phútTừ khóa: InputStream, Reader, blocking, NIO, Channel, Buffer, Selector, Files, Path, reflection, annotation, Serializable, record

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 đề

3. Giải pháp: hiểu 2 thế hệ API

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.iojava.nio
Đơn vịStream (byte/char tuần tự)Channel + Buffer (khối)
BlockingLuôn blockingSocket có thể non-blocking
Một thread nhiều connectionKhôngCó (Selector)
Đọc lùi / nhảy vị tríKhôngCó (position())
Dùng trong app thường ngàyReader/Writer có bufferFiles, Path

7. Khi nào nên dùng / không nên dùng

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
EncapsulationsetAccessible phá private; module system (Java 9+) có thể chặn
Native imageGraalVM 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ụ.

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:

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/Path của NIO.2, đọc file lớn bằng Files.lines trong 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/Path and stream large files with Files.lines inside 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 serialVersionUID và 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 serialVersionUID and never deserialize untrusted data.

Cách nhớ ngắn gọn


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).