Aplikasi yang membaca dan menulis file harus mengantisipasi berbagai kegagalan: file tidak ada, disk penuh, izin ditolak, atau serangan path traversal dari input pengguna. Tanpa pola yang konsisten, kode file menjadi rapuh dan sulit di-debug. Bab ini menggabungkan teknik-teknik yang sudah kita pelajari menjadi satu pipeline yang robust.
Enam Pilar File Processing yang Andal #
| Pola | Tujuan | API Kotlin |
|---|---|---|
use { } | Auto-close resource (Reader, Writer, Stream) | Extension function pada Closeable |
useLines { } | Streaming baca baris-per-baris, hemat memori | File.useLines { } |
| Validasi path | Cegah path traversal (../etc/passwd) | Path.normalize(), startsWith() |
try-catch IOException | Tangani kegagalan I/O secara eksplisit | catch (e: IOException) |
| Atomic write | Tulis ke temp file, lalu move atomik | Files.move(..., ATOMIC_MOVE) |
deleteIfExists | Cleanup idempoten, tidak melempar jika file tidak ada | Files.deleteIfExists() |
Pipeline Lengkap #
Mari kita rangkai semua pola di atas menjadi satu program:
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
fun main() {
// === 1. Pattern use { } untuk resource auto-close ===
val tmp = Files.createTempFile("robust-", ".txt")
try {
Files.newBufferedWriter(tmp).use { writer ->
writer.write("baris pertama\n")
writer.write("baris kedua\n")
}
// === 2. Baca dengan useLines (streaming, hemat memori) ===
Files.newBufferedReader(tmp).use { reader ->
reader.forEachLine { line -> println("Dibaca: $line") }
}
// === 3. Validasi path (cegah path traversal) ===
val userInput = "../etc/passwd"
val safeBase = Path.of("/tmp/app-data").also { Files.createDirectories(it) }
val resolved = safeBase.resolve(userInput).normalize()
if (!resolved.startsWith(safeBase)) {
println("[BLOCK] Path traversal terdeteksi: $userInput")
} else {
println("[OK] Path aman: $resolved")
}
// === 4. Penanganan IOException eksplisit ===
try {
val content = Files.readString(Path.of("/file/tidak/ada"))
} catch (e: IOException) {
println("[ERROR] Tidak bisa membaca file: ${e.message}")
}
// === 5. Atomic write (write to temp, then move) ===
val target = Path.of("/tmp/atomic-target.txt")
val tempWrite = Files.createTempFile("atomic-", ".tmp")
try {
Files.write(tempWrite, "konten baru".toByteArray())
// Atomic move (POSIX) — jika target ada, akan ditimpa
Files.move(tempWrite, target, java.nio.file.StandardCopyOption.ATOMIC_MOVE, java.nio.file.StandardCopyOption.REPLACE_EXISTING)
println("Atomic write sukses: ${Files.readString(target)}")
} catch (e: Exception) {
Files.deleteIfExists(tempWrite)
println("Atomic write gagal: ${e.message}")
} finally {
Files.deleteIfExists(target)
}
} finally {
// === 6. Cleanup idempoten ===
Files.deleteIfExists(tmp)
}
println("Pipeline robust selesai.")
}Output yang Dihasilkan:
Dibaca: baris pertama
Dibaca: baris kedua
[BLOCK] Path traversal terdeteksi: ../etc/passwd
[ERROR] Tidak bisa membaca file: /file/tidak/ada
Atomic write sukses: konten baru
Pipeline robust selesai.Mari kita bedah setiap blok kode di atas:
Files.newBufferedWriter(tmp).use { }: membuka writer, menulis, lalu menutup secara otomatis. Jika exception terjadi di dalam block, resource tetap ditutup. Inilah padanantry-with-resourcesJava.Files.newBufferedReader(tmp).use { reader -> reader.forEachLine { } }: membaca baris-per-baris tanpa memuat seluruh file ke memori. Penting untuk file besar (>100MB).safeBase.resolve(userInput).normalize(): menggabungkan base directory dengan input pengguna, lalu.normalize()menyelesaikan..dan.dalam path. Hasilnya diverifikasi harus tetap di bawahsafeBase— jika tidak, ini path traversal.catch (e: IOException): menangkap exception I/O secara spesifik. Jangan pakaicatch (e: Exception)umum karena menyembunyikan bug logic.Files.move(tempWrite, target, ATOMIC_MOVE, REPLACE_EXISTING): memindahkan file secara atomik di POSIX. Operasi ini tidak dapat di-intercept oleh proses lain, sehingga target file selalu dalam keadaan konsisten — file lama atau file baru, tidak pernah setengah-ditulis.Files.deleteIfExists(tmp): cleanup yang aman dipanggil berulang. Tidak melempar exception jika file sudah hilang.
Pattern use untuk Resource #
use { } adalah extension function pada Closeable dan AutoCloseable. Setara dengan try-with-resources di Java:
// Tanpa use { } — perlu finally manual
val reader = Files.newBufferedReader(path)
try {
reader.forEachLine { println(it) }
} finally {
reader.close()
}
// Dengan use { } — otomatis close
Files.newBufferedReader(path).use { reader ->
reader.forEachLine { println(it) }
}Kedua versi equivalent secara semantik: jika exception terjadi di dalam block, close() tetap dipanggil.
Anti-Pattern yang Sering Ditemui #
| Anti-Pattern | Masalah | Solusi |
|---|---|---|
Files.readString(hugeFile) | Memuat seluruh file ke memori | useLines { } atau BufferedReader |
Lupa use { } | Resource leak (file descriptor) | Selalu bungkus dengan use { } |
path = base + "/" + userInput | Tidak ada validasi traversal | Gunakan resolve().normalize().startsWith() |
Files.write(target, data) langsung | Bisa tinggalkan file setengah-ditulis saat crash | Tulis ke temp file, lalu move(ATOMIC_MOVE) |
Files.delete(path) | Melempar jika tidak ada | Files.deleteIfExists(path) |
Best Practice Ringkas #
- Selalu
use { }untuk setiapReader,Writer,InputStream,OutputStream. - Validasi path dari input pengguna dengan
normalize().startsWith(baseDir). - Tangkap IOException secara spesifik, bukan
Exceptionumum. - Atomic write untuk file penting: tulis ke temp, lalu move.
- Cleanup idempoten dengan
deleteIfExistsdifinallyblock. - Streaming untuk file besar dengan
useLines { }atauBufferedReader. - Logging untuk setiap kegagalan — file operation adalah sumber bug produksi yang sering.
Referensi Lanjutan #
- Java SE 21 Files
— dokumentasi
FilesJava SE 21 untuknewBufferedReader,readAllBytes, dancopy - Kotlin stdlib kotlin.io.path
— paket
kotlin.io.pathdengan extensionPathyang aman dan idiomatic
Selanjutnya #
Dengan file I/O yang robust, kita siap melangkah ke topik berikutnya: pengelolaan tanggal dan waktu dengan java.time dan kotlinx-datetime, yang merupakan bagian penting dari aplikasi nyata.