@Singleton dan @Log menghapus dua jenis boilerplate yang masih sering ditulis manual, yaitu pola satu instans (singleton) dan pencatatan peristiwa (log). Topik ini membahas akses instans tunggal melalui getInstance(), perbedaan mode eager dan lazy, penolakan pemanggilan new, serta opsi property dan strict. Dua bahasan terakhir menyusuri keluarga anotasi logging pihak ketiga dan formatter kustom yang membuat keluaran log stabil. Contoh-contohnya dijalankan pada Groovy 5.1.3, dan setiap keluaran yang tercantum adalah keluaran aslinya.
Instans tunggal dengan @Singleton#
@Singleton ditempelkan pada kelas untuk menghasilkan satu instans bersama. Anotasi ini menambahkan field statis bernama instance, metode akses getInstance(), dan konstruktor pribadi yang menjaga jalur pembuatan objek. Secara default anotasi bekerja secara eager, yaitu instans dibangun pada saat kelasnya diinisialisasi, bukan saat program mulai berjalan. Inisialisasi kelas sendiri terjadi pada sentuhan pertama terhadap kelas tersebut, sehingga pembangunan instans ikut tertunda hingga saat itu. Pemanggilan getInstance() maupun pembacaan properti instance mengembalikan objek yang sama.
import groovy.lang.Singleton
import java.lang.reflect.Modifier
// Satu instans bersama untuk seluruh program
@Singleton
class KonfigurasiAplikasi {
Map<String, String> nilai = [lingkungan: 'dev', batasWaktu: '30']
}
def bidang = KonfigurasiAplikasi.getDeclaredField('instance')
println "deklarasi field : ${Modifier.toString(bidang.modifiers)}"
println "instans tunggal : ${KonfigurasiAplikasi.instance.is(KonfigurasiAplikasi.getInstance())}"
KonfigurasiAplikasi.instance.nilai.lingkungan = 'prod'
println "setelah diubah : ${KonfigurasiAplikasi.instance.nilai.lingkungan}"
try {
new KonfigurasiAplikasi()
} catch (RuntimeException e) {
println "Gagal: ${e.getClass().simpleName}"
println "Pesan: ${e.message}"
}deklarasi field : public static final
instans tunggal : true
setelah diubah : prod
Gagal: RuntimeException
Pesan: Can't instantiate singleton KonfigurasiAplikasi. Use KonfigurasiAplikasi.instanceHasil refleksi membaca deklarasi field public static final, yang berarti instans sudah terpasang sejak kelas KonfigurasiAplikasi diinisialisasi. Perbandingan is() menghasilkan true karena instance dan getInstance() menunjuk objek yang sama, dan perubahan nilai lingkungan dari dev menjadi prod langsung terlihat pada setiap akses berikutnya terhadap instans itu. Pemanggilan new di akhir skrip gagal dengan RuntimeException berpesan Can't instantiate singleton KonfigurasiAplikasi. Use KonfigurasiAplikasi.instance. Pesan itu berasal dari konstruktor pribadi hasil anotasi, yang melempar error setiap kali instans resmi sudah ada.
Pola ini cocok untuk keadaan yang memang dimiliki satu saja dan dibagi seluruh program, misalnya konfigurasi aplikasi atau kumpulan sumber daya yang mahal dibuat. Keuntungannya, setiap pemanggil memperoleh objek yang sama tanpa harus mengoper referensinya berulang kali. Konsekuensinya adalah keadaan global, dan keadaan global punya harga pada saat pengujian.
Mode lazy dan jebakan new#
Opsi lazy = true mengubah waktu pembangunan instans. Field hasilnya menjadi private static volatile tanpa nilai awal, dan getInstance() baru membangun instans pada pemanggilan pertama. Prosesnya dilindungi dua pemeriksaan null beserta sinkronisasi terhadap objek Class, pola yang dikenal sebagai double-checked locking, sehingga aman dipanggil dari beberapa thread sekaligus. Mode ini cocok bila pembangunan instans mahal dan belum tentu dipakai.
import groovy.lang.Singleton
import java.lang.reflect.Modifier
// lazy = true: instans dibangun saat akses pertama
@Singleton(lazy = true)
class PengaturPiket {
// blok inisialisasi instans: penanda kapan objek dibangun
{
println 'instans PengaturPiket dibangun'
}
List<String> jadwal = ['Senin: Ali', 'Selasa: Budi']
}
def bidang = PengaturPiket.getDeclaredField('instance')
println "deklarasi field : ${Modifier.toString(bidang.modifiers)}"
println "sebelum akses pertama"
def liar = new PengaturPiket()
println "objek liar : ${liar.getClass().simpleName}"
println "instans resmi : ${PengaturPiket.instance.jadwal[0]}"
println "dua objek beda : ${!liar.is(PengaturPiket.instance)}"deklarasi field : private static volatile
sebelum akses pertama
instans PengaturPiket dibangun
objek liar : PengaturPiket
instans PengaturPiket dibangun
instans resmi : Senin: Ali
dua objek beda : trueDua baris instans PengaturPiket dibangun berasal dari dua pembangunan yang berbeda. Pembangunan pertama terjadi ketika skrip memanggil new sebelum akses resmi mana pun, sehingga terbentuk objek liar di luar instans tunggal. Pembangunan kedua terjadi pada akses pertama melalui instance, dan perbandingan is() menegaskan keduanya bukan objek yang sama. Jebakan ini sunyi karena tidak ada error yang muncul, tetapi keadaan yang ditulis ke objek liar tidak pernah terlihat oleh pemakai instans resmi. Karena itu, seluruh akses ke kelas singleton sebaiknya selalu melalui metode akses hasil anotasi.
Setelah akses pertama terjadi, jalur new kembali ditutup sebagaimana mode eager. Penjaganya sama, sehingga perbedaan kedua mode hanya pada waktu pembangunan instans, bukan pada penolakan new. Skrip berikut memanggil instance lebih dahulu, lalu mencoba new yang kini ditolak.
import groovy.lang.Singleton
@Singleton(lazy = true)
class PengaturPiket {
List<String> jadwal = ['Senin: Ali', 'Selasa: Budi']
}
PengaturPiket.instance
try {
new PengaturPiket()
} catch (RuntimeException e) {
println "Gagal: ${e.getClass().simpleName}"
println "Pesan: ${e.message}"
}Gagal: RuntimeException
Pesan: Can't instantiate singleton PengaturPiket. Use PengaturPiket.instanceOpsi property dan strict#
Nama field instance dan metode getInstance() bukan aturan yang mengikat. Atribut property mengganti keduanya sekaligus, sehingga @Singleton(property = 'utama') menghasilkan field utama dan metode getUtama(). Penamaan seperti ini berguna ketika pemanggil ingin mengakses instans tanpa metode akses, misalnya PengaturJadwal.utama pada skrip pemakai kelas itu. Bentuk propertinya pun mengikuti nama baru tersebut, sehingga utama dapat dibaca langsung sebagaimana properti kelas biasa. Contoh berikut memeriksa nama field, metode akses, dan pemakaian propertinya.
import groovy.lang.Singleton
// property mengganti nama field sekaligus nama metode aksesnya
@Singleton(property = 'utama')
class PengaturJadwal {
List<String> piket = ['Ali', 'Budi']
}
def bidang = PengaturJadwal.getDeclaredField('utama')
println "nama field : ${bidang.name}"
println "getUtama() : ${PengaturJadwal.getUtama().piket.join(', ')}"
println "properti : ${PengaturJadwal.utama.piket.size()}"nama field : utama
getUtama() : Ali, Budi
properti : 2Atribut strict mengatur sikap anotasi terhadap konstruktor yang ditulis manual. Nilai default strict = true membuat kompilasi gagal ketika kelas masih memiliki konstruktor, sedangkan @Singleton(strict = false) membiarkannya tetap ada. Perlu dicatat bahwa penjaga anti-new hanya terpasang pada konstruktor yang dihasilkan anotasi, sehingga kelas dengan strict = false kehilangan perlindungan itu dan pemanggilan new kembali sah. Gunakan opsi ini hanya ketika konstruktor tambahan memang diperlukan, misalnya untuk kelas yang harus tetap bisa dibangun lewat jalur biasa.
Pencatatan peristiwa dengan @Log#
@Log menambahkan field log bertipe java.util.logging.Logger yang siap dipakai di seluruh isi kelas tanpa deklarasi tambahan. Nama logger mengikuti nama kelasnya, sedangkan metode yang boleh dipanggil mengikuti level, yaitu tingkat keparahan pesan pada java.util.logging, di antaranya info, warning, dan severe. Setiap pemanggilan juga dibungkus pemeriksaan isLoggable, sehingga pembentukan pesan yang mahal dilewati ketika levelnya sedang tidak aktif. Atribut value mengganti nama field, sedangkan atribut category mengganti nama logger.
Keluaran java.util.logging pada pengaturan bawaannya kurang cocok untuk skrip yang harus stabil, karena formatnya memuat tanggal dan jam serta tercetak ke aliran error standar. Cara paling ringkas mengarahkan ulang keluarannya adalah memasang handler terpisah, karena metode setOutputStream milik ConsoleHandler bersifat protected dan tidak dapat dipanggil dari luar. Skrip berikut memasang handler dengan formatter kustom yang hanya mencetak LEVEL: pesan, lalu mematikan pewarisan ke handler bawaan melalui useParentHandlers = false.
import groovy.util.logging.Log
import java.util.logging.Formatter
import java.util.logging.Level
import java.util.logging.LogRecord
import java.util.logging.StreamHandler
@Log
class PencatatSuhu {
void catat(double suhu) {
if (suhu > 30.0) {
log.warning "suhu melampaui batas: $suhu"
} else {
log.info "suhu terbaca: $suhu"
}
}
void sensorMati() {
log.severe 'sensor tidak merespons'
}
}
// handler dan formatter kustom agar keluaran stabil tanpa tanggal
def formatter = new Formatter() {
String format(LogRecord rekaman) {
"${rekaman.level.name}: ${rekaman.message}\n".toString()
}
}
def handler = new StreamHandler(System.out, formatter) {
void publish(LogRecord rekaman) {
super.publish(rekaman)
flush()
}
}
handler.level = Level.ALL
def logger = PencatatSuhu.log
logger.addHandler(handler)
logger.useParentHandlers = false
println "field log : ${logger.name}"
def pencatat = new PencatatSuhu()
pencatat.catat(25.5)
pencatat.catat(31.0)
pencatat.sensorMati()field log : PencatatSuhu
INFO: suhu terbaca: 25.5
WARNING: suhu melampaui batas: 31.0
SEVERE: sensor tidak meresponsBaris pertama keluaran memperlihatkan nama logger yang sama dengan nama kelasnya, lalu tiga peristiwa tercetak dengan format LEVEL: pesan. Pemanggilan catat(25.5) berjalan pada level info, catat(31.0) melampaui ambang dan naik ke warning, sedangkan sensorMati() mewakili kondisi terburuk pada level severe. Seluruh keluaran bebas dari tanggal dan jam, sehingga hasilnya dapat dibandingkan langsung antar eksekusi.
Keluarga anotasi logging pihak ketiga#
Groovy menyediakan anotasi logging untuk pustaka pihak ketiga dengan pola yang sama, yaitu field log beserta metode pencatatan milik pustakanya. Perbedaannya ada pada tipe field dan sumber logger, dan seluruhnya memerlukan berkas jar pustaka tersebut di classpath. Tanpa jar itu, kelas yang dianotasi gagal dipakai saat program berjalan karena kelas pustakanya tidak ditemukan. Sandbox pada jalur ini tidak membawa jar pihak ketiga, sehingga contoh @Slf4j berikut ditandai tidak-dijalankan.
| Anotasi | Pustaka | Tipe field log |
|---|---|---|
| @Log | java.util.logging (JDK) | java.util.logging.Logger |
| @PlatformLog | platform logging JDK 9+ | java.lang.System.Logger |
| @Commons | Commons Logging | org.apache.commons.logging.Log |
| @Log4j | Log4j 1.x | org.apache.log4j.Logger |
| @Log4j2 | Log4j 2.x | org.apache.logging.log4j.Logger |
| @Slf4j | SLF4J | org.slf4j.Logger |
// tidak-dijalankan
// @Slf4j memerlukan jar SLF4J di classpath, yang tidak tersedia di sandbox ini
import groovy.util.logging.Slf4j
@Slf4j
class PencatatArsip {
void tulis(String pesan) {
log.info pesan
}
}Metodenya pun mengikuti pustaka masing-masing, misalnya error, warn, info, debug, dan trace pada @Slf4j, sedangkan @Commons dan keluarga @Log4j menambahkan fatal di ujungnya. Ketika sebuah proyek sudah memiliki pustaka logging tertentu, anotasi yang tepat membuat kelas Groovy langsung mengikuti kesepakatan proyek tanpa menulis pembentukan logger manual. Anotasi yang dipakai juga boleh berbeda antar kelas dalam satu program, meskipun menyatukannya pada satu pustaka lebih mudah dipelihara.
Menggabungkan @Singleton dan @Log#
Keduanya dapat dipakai bersama pada satu kelas, dan urutan penulisan anotasinya bebas. Kelas ArsipCadangan berikut dibuat tunggal dengan mode lazy karena instansnya hanya diperlukan saat arsip mulai dipakai, sementara setiap peristiwanya dicatat melalui log hasil @Log. Penyetelan handler dan formatter dilakukan sekali dari luar kelas, lalu seluruh isi kelas cukup memanggil log.info atau log.warning.
import groovy.lang.Singleton
import groovy.util.logging.Log
import java.util.logging.Formatter
import java.util.logging.Level
import java.util.logging.LogRecord
import java.util.logging.StreamHandler
// satu instans arsip cadangan yang mencatat setiap peristiwanya
@Singleton(lazy = true)
@Log
class ArsipCadangan {
List<String> riwayat = []
void simpan(String berkas) {
riwayat << berkas
log.info "tersimpan: $berkas"
}
void pulihkan(String berkas) {
log.warning "pemulihan langka: $berkas"
}
}
def formatter = new Formatter() {
String format(LogRecord rekaman) {
"${rekaman.level.name}: ${rekaman.message}\n".toString()
}
}
def handler = new StreamHandler(System.out, formatter) {
void publish(LogRecord rekaman) {
super.publish(rekaman)
flush()
}
}
handler.level = Level.ALL
def logger = ArsipCadangan.log
logger.addHandler(handler)
logger.useParentHandlers = false
def arsip = ArsipCadangan.instance
arsip.simpan('catatan-2026-11-01.zip')
arsip.simpan('catatan-2026-11-02.zip')
arsip.pulihkan('catatan-2026-10-30.zip')
println "riwayat : ${arsip.riwayat.size()} berkas"
println "instans : ${ArsipCadangan.instance.is(arsip)}"INFO: tersimpan: catatan-2026-11-01.zip
INFO: tersimpan: catatan-2026-11-02.zip
WARNING: pemulihan langka: catatan-2026-10-30.zip
riwayat : 2 berkas
instans : trueKombinasi ini bekerja tanpa gesekan karena kedua anotasi menyentuh bagian kelas yang berbeda, yaitu konstruksi objek untuk @Singleton dan penambahan field untuk @Log. Pada tiga pemanggilan pertama, instans tunggal terbentuk, dua berkas tersimpan ke riwayat, dan pemulihan langka tercatat pada level warning. Dua baris terakhir menegaskan hasilnya, yaitu riwayat berisi dua berkas dan instans yang dipakai memang objek tunggal yang sama. Pola seperti ini biasa dipakai untuk komponen latar belakang yang mencatat pekerjaannya sendiri, misalnya arsip cadangan berkala atau pencatat suhu yang berjalan terus. Kelas berperan sebagai sumber peristiwa, sedangkan penyetelan handler sepenuhnya berada di tangan pemakainya.
Kesalahan umum#
Kesalahan pertama adalah menambahkan konstruktor pada kelas yang dianotasi @Singleton. Anotasi ini menolaknya saat kompilasi, dan pesan errornya mengarah pada dua pilihan perbaikan. Karena error kompilasi tidak dapat ditangkap di dalam skrip yang sama, contoh berikut mengompilasi kode uji melalui GroovyShell.
import org.codehaus.groovy.control.CompilationFailedException
// error kompilasi tidak dapat ditangkap di dalam skrip yang sama,
// sehingga kode uji dikompilasi melalui GroovyShell
def shell = new GroovyShell()
def kode = '''
import groovy.lang.Singleton
@Singleton
class KolamSambungan {
KolamSambungan() { }
}
'''
try {
shell.parse(kode)
} catch (CompilationFailedException e) {
println "Gagal: ${e.getClass().simpleName}"
println "Pesan: ${e.message.readLines().find { it.contains('additional constructors') }}"
}Gagal: MultipleCompilationErrorsException
Pesan: Script1.groovy: 6: @Singleton didn't expect to find one or more additional constructors: remove constructor(s) or set strict=falseAda dua jalan keluar sesuai pesan tersebut, yaitu menghapus konstruktor dan memindahkan inisialisasinya ke deklarasi field atau blok inisialisasi instans, atau menuliskan @Singleton(strict = false) bila konstruktor tambahan memang diperlukan.
Kesalahan kedua adalah menaruh keadaan yang sering berubah di dalam singleton tanpa pengaturan ulang. Satu instans dipakai bersama seluruh program, sehingga keadaan yang diubah satu bagian langsung terlihat oleh bagian lain, dan pada pengujian keadaan itu terbawa antar kasus tanpa pesan apa pun. Kegagalan seperti ini juga sulit dilacak karena tidak ada error yang muncul, melainkan hasil yang berubah mengikuti urutan eksekusi. Perbaikannya bukan membuang polanya seluruhnya, melainkan memisahkan keadaan yang memang tetap, seperti konfigurasi, dari keadaan yang berputar, seperti daftar pekerjaan. Yang terakhir ditaruh di objek biasa yang dibuat ulang.
Ringkasan#
- @Singleton menghasilkan field
instance, metodegetInstance(), dan konstruktor penjaga, dengan mode eager sebagai default danlazy = trueuntuk pembangunan saat akses pertama. - Pemanggilan
newditolak denganRuntimeExceptionsetelah instans resmi ada, tetapi pada singleton lazy sebelum akses pertama justru membentuk objek liar, sehingga akses selalu melalui metode aksesnya. - Atribut
propertymengganti nama field dan metode akses, sedangkanstrict = falsemembiarkan konstruktor manual dengan konsekuensi kehilangan penjaganew. - @Log menambahkan field
logbertipejava.util.logging.Loggerdengan metodeinfo,warning, dansevere, dan keluaran yang stabil diperoleh lewat handler serta formatter kustom.
Referensi#
- Singleton (groovy.lang) — Apache Groovy — Dokumentasi API anotasi @Singleton beserta atribut lazy, property, dan strict.
- Log (groovy.util.logging) — Apache Groovy — Dokumentasi API anotasi @Log beserta atribut value dan category.
- Compile-time metaprogramming — Apache Groovy — Daftar transformasi AST resmi yang memuat @Singleton dan keluarga anotasi logging.
- Slf4j (groovy.util.logging) — Apache Groovy — Dokumentasi anotasi @Slf4j yang mewakili keluarga anotasi logging pihak ketiga.