Cara paling ringkas mengirim dan menerima pesan JMS di Java modern adalah memakai JMSContext, API sederhana yang diperkenalkan di JMS 2.0 dan dipertahankan di Jakarta Messaging 3.x. Anda membuat connection factory, membuka JMSContext dalam blok try-with-resources, lalu memakai JMSProducer untuk mengirim dan JMSConsumer untuk menerima. Begitu blok try selesai, koneksi ditutup otomatis.
Contoh di artikel ini memakai package jakarta.jms. Jika proyek Anda masih di Java EE 8 atau library lama, gantilah dengan javax.jms; nama kelas dan method-nya sama. Brokernya ActiveMQ Artemis yang berjalan di localhost:61616, dengan library klien artemis-jakarta-client dari grup Maven org.apache.activemq. Konsep dasar seperti queue, topic, dan connection factory dibahas di artikel pengantar JMS.
1. Menyiapkan connection factory
Connection factory menyimpan alamat broker dan kredensial. Buat satu kali dan pakai ulang di seluruh aplikasi. Jangan menulis kata sandi langsung di kode; ambil dari environment variable atau secret manager.
import jakarta.jms.ConnectionFactory;
import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory;
public final class Broker {
private Broker() {}
public static ConnectionFactory connectionFactory() {
String url = System.getenv().getOrDefault("BROKER_URL", "tcp://localhost:61616");
String user = System.getenv("BROKER_USER");
String password = System.getenv("BROKER_PASSWORD");
return new ActiveMQConnectionFactory(url, user, password);
}
}
2. Producer: mengirim pesan
import jakarta.jms.*;
public class PesananProducer {
private final ConnectionFactory cf;
public PesananProducer(ConnectionFactory cf) {
this.cf = cf;
}
public void kirim(String orderId, String json) {
try (JMSContext context = cf.createContext()) {
Queue queue = context.createQueue("pesanan.baru");
context.createProducer()
.setDeliveryMode(DeliveryMode.PERSISTENT)
.setTimeToLive(24L * 60 * 60 * 1000)
.setProperty("orderId", orderId)
.send(queue, json);
}
}
}
Beberapa hal yang terjadi di sini:
createQueuetidak membuat queue di broker; method ini hanya membuat objek yang merujuk ke nama queue. Apakah queue dibuat otomatis tergantung konfigurasi broker. Artemis membuatnya otomatis dalam konfigurasi bawaannya.setDeliveryMode(PERSISTENT)sebenarnya sudah nilai bawaan, tetapi menuliskannya membuat niat kode jelas.setTimeToLivemembuat pesan kedaluwarsa bila tidak diproses dalam 24 jam.setPropertymenambahkan property yang bisa dipakai consumer untuk menyaring pesan dengan selector tanpa membaca body.- Method di JMSContext melempar
JMSRuntimeException(unchecked), berbeda dengan API klasik yang melemparJMSException(checked).
Membuka JMSContext untuk setiap pesan seperti contoh di atas mudah dibaca, tetapi mahal bila volumenya tinggi karena setiap kali membuka koneksi baru. Untuk aplikasi sungguhan, gunakan connection pool (misalnya pooled connection factory dari framework Anda) atau simpan satu JMSContext per thread.
3. Consumer sinkron: menarik pesan
Consumer sinkron cocok untuk proses batch atau alat baris perintah. Ia memanggil receive dan menunggu sampai pesan datang atau batas waktu habis.
try (JMSContext context = cf.createContext();
JMSConsumer consumer = context.createConsumer(context.createQueue("pesanan.baru"))) {
while (true) {
Message message = consumer.receive(5000);
if (message == null) {
break; // tidak ada pesan dalam 5 detik
}
String json = message.getBody(String.class);
String orderId = message.getStringProperty("orderId");
prosesPesanan(orderId, json);
}
} catch (JMSException e) {
throw new IllegalStateException("Gagal membaca pesan", e);
}
Jika Anda hanya butuh isi pesan, consumer.receiveBody(String.class, 5000) langsung mengembalikan body. Panggilan receive() tanpa argumen akan menunggu selamanya, jadi pakai versi dengan timeout agar program bisa berhenti dengan rapi. JMSContext otomatis memulai koneksi ketika consumer pertama dibuat, jadi Anda tidak perlu memanggil start() secara manual.
4. Consumer asinkron dengan transaksi
Untuk layanan yang terus berjalan, daftarkan listener. Broker mendorong pesan ke listener begitu tersedia. Contoh berikut memakai sesi transaksi: commit() menandai pesan selesai, rollback() mengembalikannya ke broker untuk dikirim ulang sesuai aturan redelivery.
JMSContext context = cf.createContext(JMSContext.SESSION_TRANSACTED);
JMSConsumer consumer = context.createConsumer(context.createQueue("pesanan.baru"));
consumer.setMessageListener(message -> {
try {
prosesPesanan(message.getStringProperty("orderId"), message.getBody(String.class));
context.commit();
} catch (Exception e) {
log.warn("Pemrosesan gagal, pesan dikembalikan ke broker", e);
context.rollback();
}
});
// simpan referensi context dan tutup saat aplikasi berhenti
Ingat bahwa setMessageListener tidak boleh dipakai di container web atau EJB Jakarta EE. Di sana gunakan message-driven bean, atau di Spring gunakan @JmsListener yang mengelola thread dan transaksinya untuk Anda.
5. Memilih acknowledge mode
| Mode | Perilaku | Kapan dipakai |
|---|---|---|
AUTO_ACKNOWLEDGE |
Pesan diakui otomatis setelah receive kembali atau listener selesai |
Bawaan; cocok untuk pemrosesan sederhana |
CLIENT_ACKNOWLEDGE |
Kode memanggil acknowledge() sendiri |
Saat Anda ingin mengakui setelah kerja selesai. Perhatikan: satu panggilan mengakui semua pesan yang sudah dikonsumsi sesi itu, bukan hanya satu pesan |
DUPS_OK_ACKNOWLEDGE |
Pengakuan dilakukan secara malas | Beban rendah, tetapi duplikat lebih mungkin; hanya untuk consumer yang benar-benar idempoten |
SESSION_TRANSACTED |
Pesan diakui saat commit(), rollback memicu pengiriman ulang |
Bila consumer juga mengirim pesan lanjutan yang harus ikut dalam satu transaksi |
6. Menutup resource dengan benar
Kebocoran koneksi adalah masalah klasik. Gejalanya: jumlah koneksi di konsol broker terus naik, lalu suatu saat broker menolak koneksi baru. Beberapa aturan praktis:
- Gunakan try-with-resources untuk JMSContext dan JMSConsumer berumur pendek; keduanya mengimplementasikan
AutoCloseable. - Untuk listener berumur panjang, simpan referensi JMSContext dan tutup di hook shutdown aplikasi, misalnya
Runtime.getRuntime().addShutdownHookatau mekanisme lifecycle framework Anda. - Jangan memanggil
close()ataustop()pada JMSContext dari dalam listener miliknya sendiri; spesifikasi melarangnya dan hasilnya bisa berupa exception atau aplikasi yang menggantung. - JMSContext tidak thread-safe. Satu thread, satu context. Connection factory boleh dipakai bersama.
Kesalahan umum saat mulai
- Tipe body tidak cocok.
getBody(String.class)hanya bekerja untukTextMessage. Jika pengirim memakaiBytesMessage, Anda akan mendapatMessageFormatException. Sepakati jenis pesan dan format body, misalnya JSON UTF-8, dengan tim pengirim. - Mencampur javax dan jakarta. Library klien lama berbasis
javax.jmstidak bisa dipasangkan dengan kode yang mengimporjakarta.jms. Gejalanya biasanyaClassCastExceptionatauNoClassDefFoundErrorsaat aplikasi berjalan. - Listener yang menelan exception. Menangkap semua exception lalu tetap commit berarti pesan yang gagal hilang tanpa jejak. Bedakan kegagalan yang perlu diulang dan yang harus diparkir di dead letter queue.
- Tidak memasang exception listener. Untuk consumer berumur panjang, daftarkan
context.setExceptionListener(...)agar Anda tahu ketika koneksi ke broker putus dan bisa menyambung ulang, atau pastikan framework Anda menangani hal ini.
Menguji kode ini
Jalankan broker lokal dengan Docker, kirim beberapa pesan, lalu matikan consumer di tengah pemrosesan untuk melihat redelivery terjadi. Coba juga lempar exception di listener transaksi dan amati pesan yang sama datang lagi. Untuk pengujian otomatis yang bisa jalan di CI, lihat uji integrasi message broker dengan Testcontainers dan JUnit 5.