Skip to content

Add @AmqpListener annotation support#3357

Merged
cppwfs merged 2 commits intospring-projects:mainfrom
artembilan:AmqpListenerAnnotation
Feb 24, 2026
Merged

Add @AmqpListener annotation support#3357
cppwfs merged 2 commits intospring-projects:mainfrom
artembilan:AmqpListenerAnnotation

Conversation

@artembilan
Copy link
Copy Markdown
Member

  • Extract AbstractListenerAnnotationBeanPostProcessor into the core spring-amqp module from the RabbitListenerAnnotationBeanPostProcessor for the common infrastructure and API which can be used in other implementations.
  • Implement AmqpListenerAnnotationBeanPostProcessor as an extension of just extracted AbstractListenerAnnotationBeanPostProcessor
  • The AmqpListenerAnnotationBeanPostProcessor processes bean methods with @AmqpListener
  • The @AmqpListeners is a @Repeatable container for @AmqpListener
  • The MethodAmqpListenerEndpoint is an AbstractAmqpListenerEndpoint extension for POJO method-based listeners.
    The @AmqpListener parsing in the AmqpListenerAnnotationBeanPostProcessor creates MethodAmqpListenerEndpoint instances for methods they re declared on
  • The MethodAmqpMessageListenerContainerFactory is an extension of the AmqpMessageListenerContainerFactory with POJO method-specific listeners
  • Extract simple record BytesToStringConverter into spring-amqp module and replace internal type in the RabbitListenerAnnotationBeanPostProcessor
  • Fix nullability for the RabbitListenerAnnotationBeanPostProcessor
  • Fix TODO in the AmqpListenerEndpoint.getId() Javadoc: the final id generation decision in done at the moment of bean registration in the AmqpListenerEndpointRegistry
  • Add AmqpMessageListenerContainerFactory.configureEndpoint() for target extensions where final endpoint properties are merged with whatever is provided as default values in the container factory
  • Fix AmqpMessageListenerContainerFactory.createContainer() moving setupMessageListener() call in the end.
    Essentially, all the properties must be set into the listener container before we provide a listener over there
  • Expose AmqpListenerAnnotationBeanPostProcessor as a bean in the AmqpDefaultConfiguration
  • Add io.projectreactor:reactor-core as test dependency for request-reply test with a Mono return type

* Extract `AbstractListenerAnnotationBeanPostProcessor` into the core `spring-amqp` module
from the `RabbitListenerAnnotationBeanPostProcessor` for the common infrastructure and
API which can be used in other implementations.
* Implement `AmqpListenerAnnotationBeanPostProcessor` as an extension of just
extracted `AbstractListenerAnnotationBeanPostProcessor`
* The `AmqpListenerAnnotationBeanPostProcessor` processes bean methods with `@AmqpListener`
* The `@AmqpListeners` is a `@Repeatable` container for `@AmqpListener`
* The `MethodAmqpListenerEndpoint` is an `AbstractAmqpListenerEndpoint` extension
for POJO method-based listeners.
The `@AmqpListener` parsing in the `AmqpListenerAnnotationBeanPostProcessor`
creates `MethodAmqpListenerEndpoint` instances for methods they re declared on
* The `MethodAmqpMessageListenerContainerFactory` is an extension of the
`AmqpMessageListenerContainerFactory` with POJO method-specific listeners
* Extract simple `record BytesToStringConverter` into `spring-amqp` module
and replace internal type in the `RabbitListenerAnnotationBeanPostProcessor`
* Fix nullability for the `RabbitListenerAnnotationBeanPostProcessor`
* Fix TODO in the `AmqpListenerEndpoint.getId()` Javadoc:
the final id generation decision in done at the moment of bean registration
in the `AmqpListenerEndpointRegistry`
* Add `AmqpMessageListenerContainerFactory.configureEndpoint()`
for target extensions where final endpoint properties are merged
with whatever is provided as default values in the container factory
* Fix `AmqpMessageListenerContainerFactory.createContainer()`
moving `setupMessageListener()` call in the end.
Essentially, all the properties must be set into the listener container
before we provide a listener over there
* Expose `AmqpListenerAnnotationBeanPostProcessor` as a bean in the `AmqpDefaultConfiguration`
* Add `io.projectreactor:reactor-core` as test dependency for request-reply test with a `Mono`
return type
@artembilan artembilan added this to the 4.1.0-M3 milestone Feb 23, 2026
@artembilan artembilan requested a review from cppwfs February 23, 2026 23:34
@artembilan
Copy link
Copy Markdown
Member Author

TODO (which could be done in the separate PRs)

  1. Kotlin suspend functions tests
  2. Docs
  3. @AmqpHandler for type-base routing with a common @AmqpListener on the class. But I have doubts for this so far. We can come back here eventually.

I mean there are still more like sendAndReceive() on the client, streaming support, but that is not related to the annotation.

Thanks

Copy link
Copy Markdown
Contributor

@cppwfs cppwfs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!
Just some nitpicks.

}
}

protected @Nullable Integer resolveExpressiontoInteger(String value, String attribute) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to should be capitalized

methods.add(new ListenerMethod<>(method, listenerAnnotations));
}
if (hasClassLevelListeners) {
Annotation rabbitHandler = AnnotationUtils.findAnnotation(method, handlerAnnotation);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

amqpHandler instead of rabbitHandler.

protected static String noBeanFoundMessage(Object target, String listenerBeanName, String requestedBeanName,
Class<?> expectedClass) {

return "Could not register rabbit listener endpoint on ["
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AMQP instead of rabbit

/**
* The AMQP 1.0 addresses to listen to.
* The entries can be 'queue name', 'property-placeholder keys' or 'expressions'.
* Expression must be resolved to the address.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this say that Each expression must be resolved to an address?

* The entries can be 'queue name', 'property-placeholder keys' or 'expressions'.
* Expression must be resolved to the address.
* The addresses must exist.
* @return the addresses or expressions (SpEL) to listen to from in the target listener container.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to listen to from in to to listen to in

/**
* Override the container factory's {@code headerMapper} used for this listener.
* @return the header mapper bean name.
* If a SpEL expression is provided ({@code #{...}}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto


/**
* Set the default behavior for messages rejection, for example, when the listener
* threw an exception. When {@code true} (default), messages will be requeued, otherwise - rejected.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

threw should be throws


private @Nullable BeanResolver beanResolver;

public MethodAmqpListenerEndpoint(Object bean, Method method, String... addresses) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing Javadoc.

if (resolved != null && beanType.isAssignableFrom(resolved.getClass())) {
return (B) resolved;
}
else if (resolved instanceof String factoryBeanName && StringUtils.hasText(factoryBeanName)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't we want else if (resolved != null && resolve instance of...?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The instanceof operator does the trick for null.
So, if resolved == null, then instanceof resolves to false.

/**
* A method annotated with {@link A}, together with the annotations.
*
* @param method the method with annotations
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

odd tab here

* Resolve copy/paste artifacts in the `AbstractListenerAnnotationBeanPostProcessor`:
replace `rabbit` with empty string - the `handler` and `listener` are enough for the context
* Add Javadoc to `MethodAmqpListenerEndpoint` ctor
@artembilan artembilan requested a review from cppwfs February 24, 2026 16:09
Copy link
Copy Markdown
Contributor

@cppwfs cppwfs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM Great work!

@cppwfs cppwfs merged commit 7455fc5 into spring-projects:main Feb 24, 2026
3 checks passed
@artembilan artembilan deleted the AmqpListenerAnnotation branch February 25, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants