MOP

Hook metaprogramming runtime: methodMissing, getProperty, dan invokeMethod.

Daftar isi

Topik ini membahas protokol metaobjek (metaobject protocol, MOP) yang menjadi dasar sifat dinamis Groovy, yaitu lapisan yang menengahi setiap pemanggilan metode dan akses properti saat program berjalan. Pembahasan mencakup antarmuka GroovyObject, hook untuk metode berupa invokeMethod dan methodMissing, hook untuk properti berupa getProperty dan setProperty, serta pola memoisasi yang menjaga biaya hook tetap terkendali. Sebuah hook adalah metode yang dipanggil otomatis oleh runtime pada situasi tertentu, misalnya ketika metode yang diminta tidak ditemukan. Diagram alur dan tiga berkas demo disertakan dengan keluaran asli, sehingga setiap langkah alur dapat ditelusuri dari kode sampai keluarannya.

Protokol metaobjek dan alur panggilan#

GroovyObject adalah antarmuka yang diimplementasikan secara implisit oleh setiap kelas Groovy. Antarmuka ini memuat empat metode, yaitu invokeMethod() untuk pemanggilan metode, getProperty() dan setProperty() untuk akses properti, serta getMetaClass() untuk mengambil metakelas milik objek. Metaclass adalah objek yang menyimpan daftar metode dan properti yang sah untuk suatu tipe, sehingga keputusan panggilan tidak perlu dikunci saat kompilasi. Karena seluruh panggilan melewati metakelas, kelas dapat mengubah perilakunya tanpa disusun ulang.

Ketika sebuah metode dipanggil, runtime menjalankan urutan pencarian yang tetap. Panggilan pertama diperiksa apakah kelasnya menerapkan GroovyInterceptable, dan bila ya, seluruh panggilan masuk ke invokeMethod. Selanjutnya metode dicari pada metakelas, lalu pada hook methodMissing apabila tidak ditemukan. Apabila tidak ada lapisan yang menangani panggilan tersebut, MissingMethodException dilemparkan. Diagram berikut merangkum alur pencarian metode tersebut.

panggilaninvokeMethodsaat GroovyInterceptablemetaClasspencarian metodemethodMissingbila metode tidak ditemukanMissingMethodExceptiontanpa penanganan
Alur pencarian metode pada pemanggilan dinamis Groovy

Intersepsi metode dengan invokeMethod#

Intersepsi adalah teknik menangkap panggilan metode sebelum panggilan itu dijawab oleh implementasi aslinya. Kelas yang menimpa invokeMethod memperoleh kendali atas panggilan tersebut, dan tingkat kendalinya bergantung pada penerapan GroovyInterceptable. Tanpa antarmuka itu, invokeMethod hanya menerima panggilan yang metodenya tidak ditemukan, sedangkan metode yang ada tetap dijawab secara normal. Parameter kedua hook berupa larik Object, sehingga seluruh argumen panggilan tersedia untuk dicatat, diubah, atau diteruskan.

Berkas berikut memuat dua kelas yang menimpa invokeMethod. Kelas Layanan adalah kelas biasa, sedangkan kelas Pelacak menerapkan GroovyInterceptable. Keduanya memiliki metode sapa yang sama, sehingga perbedaan keluaran sepenuhnya berasal dari perbedaan penerapan intersepsinya.

groovy intersepsi.groovy
// tidak-dijalankan
// invokeMethod pada kelas biasa hanya menerima panggilan yang
// tidak menemukan metodenya.
class Layanan {
    def invokeMethod(String nama, Object args) {
        return "tertangkap: ${nama}(${args.join(', ')})"
    }

    String sapa(String nama) {
        return "Halo, ${nama}!"
    }
}

def l = new Layanan()
println l.sapa('Budi')
println l.singkirkan(1, 2)

// GroovyInterceptable memaksa semua panggilan masuk invokeMethod
class Pelacak implements GroovyInterceptable {
    def invokeMethod(String nama, Object args) {
        return "tercatat: ${nama}"
    }

    String sapa(String nama) {
        return "Halo, ${nama}!"
    }
}

def p = new Pelacak()
println p.sapa('Citra')
bash
# menjalankan demo intersepsi metode
groovy intersepsi.groovy
text Keluaran
Halo, Budi!
tertangkap: singkirkan(1, 2)
tercatat: sapa

Baris pertama membuktikan bahwa metode sapa yang benar-benar ada tetap dijawab oleh implementasinya tanpa melewati invokeMethod. Baris kedua menunjukkan bahwa panggilan singkirkan yang tidak memiliki implementasi jatuh ke invokeMethod sebagai penangkap cadangan, lengkap dengan nama metode dan argumennya. Baris ketiga berasal dari kelas Pelacak, dan teks tercatat: sapa menegaskan bahwa penerapan GroovyInterceptable memaksa metode yang ada sekalipun untuk melewati hook.

Hook properti getProperty dan setProperty#

Akses properti dari luar kelas dapat ditangkap dengan menimpa getProperty dan setProperty. Kedua hook ini menerima nama properti yang diminta, sehingga kelas dapat mencatat, memvalidasi, atau mengalihkan setiap pembacaan dan penugasan. Hook setProperty bertugas menerima nilai dan bertipe void, sedangkan getProperty wajib mengembalikan nilai yang akan diterima pemanggil. Hook berlaku untuk seluruh properti, baik yang benar-benar ada maupun yang tidak pernah dideklarasikan, sehingga kelas dapat menampung data yang skemanya baru diketahui saat program berjalan. Kedua hook hanya menangani akses properti, dan tidak menyentuh pemanggilan metode yang memiliki hook tersendiri.

groovy properti-hook.groovy
// tidak-dijalankan
// getProperty dan setProperty menangkap akses properti dari luar kelas.
class Konfigurasi {
    Map<String, String> isi = [:]

    void setProperty(String nama, Object nilai) {
        println "setProperty menangkap ${nama}"
        isi[nama] = nilai
    }

    def getProperty(String nama) {
        println "getProperty menangkap ${nama}"
        return isi[nama]
    }
}

def k = new Konfigurasi()
k.mode = 'gelap'
println k.mode
bash
# menjalankan demo hook properti
groovy properti-hook.groovy
text Keluaran
setProperty menangkap mode
getProperty menangkap mode
gelap

Penugasan k.mode = 'gelap' memicu setProperty terlebih dahulu, kemudian pembacaan k.mode memicu getProperty, dan nilai gelap akhirnya tercetak dari wadah di dalam kelas. Penyimpanannya sengaja memakai Map, bukan properti kelas, karena akses field di dalam badan kelas berjalan langsung tanpa melewati hook. Penyimpanan berbasis Map membuat hook bekerja sebagai satu-satunya jalur data. Dengan pola ini, urutan tangkapan pada keluaran selalu konsisten dan tidak ada nilai yang lolos tanpa tercatat.

methodMissing dan pola memoisasi#

Hook methodMissing dipanggil hanya ketika metode yang diminta tidak ditemukan pada metakelas. Ia menerima nama metode beserta argumennya, sehingga kelas dapat membuat implementasi atas nama tersebut lalu menjalankannya. Pola memoisasi (memoization) melengkapi hook ini dengan cache, yaitu tempat menyimpan hasil kerja yang sudah dibuat agar dapat dipakai ulang, sehingga pembuatan metode hanya terjadi satu kali untuk setiap nama.

groovy memo-method-missing.groovy
// tidak-dijalankan
// Memoisasi methodMissing: pembuatan metode hanya terjadi sekali.
class Kalkulator {
    private Map<String, Closure> cache = [:]

    def methodMissing(String nama, Object args) {
        Closure f = cache[nama]
        if (f == null) {
            println "membuat metode ${nama}"
            int faktor = nama.substring(4) as int
            f = { int x -> x * faktor }
            cache[nama] = f
        }
        return f(*args)
    }
}

def k = new Kalkulator()
println k.kali7(6)   // panggilan pertama: pembuatan lewat methodMissing
println k.kali7(5)   // panggilan kedua: memakai metode di cache
println k.kali3(4)
println k.kali3(2)
bash
# menjalankan demo memoisasi methodMissing
groovy memo-method-missing.groovy
text Keluaran
membuat metode kali7
42
35
membuat metode kali3
12
6

Keluaran di atas memperlihatkan perbedaan panggilan pertama dan kedua secara langsung. Teks membuat metode kali7 hanya tercetak satu kali, yaitu saat kali7 dipanggil pertama kali dan closure-nya dibuat lalu disimpan ke cache. Panggilan kali7(5) berikutnya langsung memakai closure dari cache tanpa mencetak apa pun, dan pola yang sama terulang untuk kali3. Tanpa cache, teks pembuatan metode akan tercetak pada keempat panggilan, karena setiap panggilan metode yang tidak ada selalu jatuh ke methodMissing.

Kesalahan umum#

Kesalahan pertama adalah rekursi tak berujung di dalam hook properti. Kelas yang menimpa getProperty memang mengakses properti dari badan hooknya sendiri, dan apabila properti yang dibaca bukan field yang dideklarasikan, pembacaan itu jatuh kembali ke getProperty yang sama tanpa henti. Runtime akhirnya melempar java.lang.StackOverflowError. Perbaikannya adalah menyimpan data pada Map sebagaimana contoh di atas, atau memastikan hook hanya membaca field yang benar-benar dideklarasikan pada kelas. Keduanya tidak memicu rekursi karena akses field di dalam badan kelas berjalan langsung tanpa melewati hook.

Kesalahan kedua adalah mengandalkan methodMissing pada kode yang diperiksa secara statis. Pemanggilan di bawah anotasi seperti @CompileStatic diselesaikan saat kompilasi, sehingga hook tidak pernah mendapat kesempatan dipanggil dan yang muncul justru pesan error kompilasi. Panggilan yang mengandalkan hook selalu dilakukan dari kode dinamis, misalnya dari metode yang sengaja dibebaskan dari pemeriksaan statis dengan anotasi khusus. Kedua kesalahan di atas bersumber dari satu akar yang sama, yaitu mengasumsikan perilaku hook berlaku pada jalur yang sebenarnya tidak melewatinya.

Ringkasan#

  • Setiap kelas Groovy secara implisit mengimplementasikan GroovyObject, dan setiap panggilan metode melewati metakelas sebelum dijawab.
  • invokeMethod menangkap panggilan yang tidak ditemukan, dan seluruh panggilan apabila kelasnya menerapkan GroovyInterceptable.
  • getProperty dan setProperty menengahi seluruh akses properti dari luar kelas, sedangkan akses field di dalam badan kelas berjalan langsung.
  • Pola memoisasi menyimpan metode yang dibuat oleh methodMissing ke dalam cache, sehingga biaya pembuatan hanya dibayar pada panggilan pertama.

Referensi#