Cara Kirim dan Terima Pesan JMS dengan Java (JMSContext)

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:

  • createQueue tidak 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.
  • setTimeToLive membuat pesan kedaluwarsa bila tidak diproses dalam 24 jam.
  • setProperty menambahkan property yang bisa dipakai consumer untuk menyaring pesan dengan selector tanpa membaca body.
  • Method di JMSContext melempar JMSRuntimeException (unchecked), berbeda dengan API klasik yang melempar JMSException (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().addShutdownHook atau mekanisme lifecycle framework Anda.
  • Jangan memanggil close() atau stop() 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 untuk TextMessage. Jika pengirim memakai BytesMessage, Anda akan mendapat MessageFormatException. Sepakati jenis pesan dan format body, misalnya JSON UTF-8, dengan tim pengirim.
  • Mencampur javax dan jakarta. Library klien lama berbasis javax.jms tidak bisa dipasangkan dengan kode yang mengimpor jakarta.jms. Gejalanya biasanya ClassCastException atau NoClassDefFoundError saat 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.

Hendra Kurniawan

Oleh Hendra Kurniawan

Pengembang backend Java yang lama menangani integrasi sistem dan message broker. Menulis catatan praktis tentang JMS, API, dan arsitektur aplikasi.