Getting started

Install

Artifacts are published to the AceMQ Maven repository. Add it alongside your other repositories — no credentials are needed.

<repositories>
  <repository>
    <id>acemq</id>
    <url>https://acemq-company.github.io/maven/</url>
  </repository>
</repositories>

<dependencies>
  <dependency>
    <groupId>org.acemq</groupId>
    <artifactId>acemq-amqp-core</artifactId>
    <version>0.7.15</version>
  </dependency>
  <dependency>
    <groupId>org.acemq</groupId>
    <artifactId>acemq-transport-rabbitmq</artifactId>
    <version>0.7.15</version>
    <scope>runtime</scope>
  </dependency>
</dependencies>

Gradle:

repositories {
    mavenCentral()
    maven { url = uri("https://acemq-company.github.io/maven/") }
}

dependencies {
    implementation("org.acemq:acemq-amqp-core:0.7.15")
    runtimeOnly("org.acemq:acemq-transport-rabbitmq:0.7.15")
}

Two artifacts, and the split is deliberate. acemq-amqp-core is the engine and knows nothing about RabbitMQ; acemq-transport-rabbitmq is the transport, discovered at runtime by the amqp:// scheme in your URL. That is why it is a runtime dependency: your code should not compile against it.

Java 17 or later.

A broker to talk to

docker run -d --name rabbit -p 5672:5672 -p 15672:15672 rabbitmq:4-management

The management UI is at http://localhost:15672, guest/guest. Worth having open the first time — everything this library does shows up there.

Send and receive

import org.acemq.amqp.core.AceMq;

public record Order(String id, double total) { }

try (AceMq mq = AceMq.connect("amqp://localhost")) {
    mq.declareExchange("orders", "topic");
    mq.declareQueue("orders.new");
    mq.bind("orders.new", "orders", "order.*");

    mq.consume("orders.new", Order.class,
            message -> System.out.println("got " + message.payload()));

    mq.publisher("orders", "order.placed", Order.class)
      .send(new Order("o-1", 42.00));

    Thread.sleep(1000);   // the consumer runs on its own threads
}

That prints got Order[id=o-1, total=42.0].

Declaring topology as data instead

The three declare calls are the shortest thing that works, and they are what every AMQP client offers. For anything you deploy, describe the topology as a value and let AceMQ work out what applying it would do:

Topology orders = Topology.define()
        .exchange("orders", "topic")
        .queue("orders.new")                       // durable quorum
        .bind("orders.new", "orders", "order.*")
        .build();

mq.topology().apply(orders, ApplyMode.DRY_RUN);    // print the plan, change nothing
mq.topology().apply(orders, ApplyMode.CREATE_ONLY);

Same result, but the topology is now something you can print in a pull request, validate against an environment you do not provision, and — the part that matters in production — check before touching anything, so a queue that already exists with different settings is reported with the offending argument named rather than failing partway through and closing the channel.

Topology covers the modes and what drift looks like.

What just happened

More than the code suggests, and all of it on purpose:

About that URL

amqp://localhost is plaintext, which is right for a broker running on your own machine and wrong for anything else. In production the URL is amqps://, and the certificate is verified by default:

AceMq.connect("amqps://broker.internal:5671");

Credentials belong in a CredentialsProvider rather than in the URL — see Security, which also states plainly which parts of the security story are not built yet.

Where to go next

Publishing options and what a confirm really promises: Publishing. Prefetch, concurrency and acknowledgement: Consuming. What happens when a handler keeps failing: Reliability.

And before you write a test against a real broker, read Testing — there is an in-memory transport that needs no Docker.