Skip to content

@BackOff does not recognize the __listener placeholder #4232

@Krejtcha

Description

@Krejtcha

In what version(s) of Spring for Apache Kafka are you seeing this issue?

Spring Boot 4.0.0, 4.0.1

Describe the bug

Having annotations like this on a listener method:

@RetryableTopic(
    backOff = @BackOff(delayString = "#{__listener.config.backOffDelay.toMillis()}"),
    attempts = "#{__listener.config.attempts}",
    dltTopicSuffix = "#{__listener.config.dltTopicSuffix}")
@KafkaListener(topics = "#{__listener.config.topics}")

The __listener placeholder is recognized for attempts , dltTopicSuffix and topics, but when used also in @BackOff delayString the following error occurs while starting the application:

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'classified': Expression parsing failed
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:610)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:525)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:333)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:371)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:331)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:196)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.instantiateSingleton(DefaultListableBeanFactory.java:1218)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingleton(DefaultListableBeanFactory.java:1184)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:1121)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:993)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:620)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:756)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:445)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:321)
	at org.springframework.boot.test.context.SpringBootContextLoader.lambda$loadContext$2(SpringBootContextLoader.java:156)
	at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:58)
	at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:46)
	at org.springframework.boot.SpringApplication.withHook(SpringApplication.java:1465)
	at org.springframework.boot.test.context.SpringBootContextLoader$ContextLoaderHook.run(SpringBootContextLoader.java:605)
	at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:156)
	at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:115)
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:247)
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.lambda$loadContext$0(DefaultCacheAwareContextLoaderDelegate.java:167)
	... 21 more
Caused by: org.springframework.beans.factory.BeanExpressionException: Expression parsing failed
	at org.springframework.context.expression.StandardBeanExpressionResolver.evaluate(StandardBeanExpressionResolver.java:185)
	at org.springframework.beans.factory.config.EmbeddedValueResolver.resolveStringValue(EmbeddedValueResolver.java:55)
	at org.springframework.kafka.annotation.BackOffFactory.resolve(BackOffFactory.java:117)
	at org.springframework.kafka.annotation.BackOffFactory.resolveDuration(BackOffFactory.java:87)
	at org.springframework.kafka.annotation.BackOffFactory.createFromAnnotation(BackOffFactory.java:60)
	at org.springframework.kafka.annotation.RetryableTopicAnnotationProcessor.processAnnotation(RetryableTopicAnnotationProcessor.java:151)
	at org.springframework.kafka.annotation.RetryTopicConfigurationProvider.findRetryConfigurationFor(RetryTopicConfigurationProvider.java:127)
	at org.springframework.kafka.annotation.KafkaListenerAnnotationBeanPostProcessor.processMainAndRetryListeners(KafkaListenerAnnotationBeanPostProcessor.java:530)
	at org.springframework.kafka.annotation.KafkaListenerAnnotationBeanPostProcessor.processMainAndRetryListeners(KafkaListenerAnnotationBeanPostProcessor.java:517)
	at org.springframework.kafka.annotation.KafkaListenerAnnotationBeanPostProcessor.processKafkaListener(KafkaListenerAnnotationBeanPostProcessor.java:502)
	at org.springframework.kafka.annotation.KafkaListenerAnnotationBeanPostProcessor.postProcessAfterInitialization(KafkaListenerAnnotationBeanPostProcessor.java:405)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:442)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1820)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:603)
	... 43 more
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field '__listener' cannot be found on object of type 'org.springframework.beans.factory.config.BeanExpressionContext' - maybe not public or not valid?
	at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:272)
	at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:123)
	at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:111)
	at org.springframework.expression.spel.ast.CompoundExpression.getValueRef(CompoundExpression.java:60)
	at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:96)
	at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:114)
	at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:267)
	at org.springframework.context.expression.StandardBeanExpressionResolver.evaluate(StandardBeanExpressionResolver.java:182)
	... 56 more

To Reproduce

Have a kafka listener e.g. like this:

public class ExampleListener {

    private final Config config;

    public ExampleListener () {
        // only for test purposes, in reality a more complex object is used and provided in the constructor,
        // its content is based on hierarchy of application properties and customizers
        // also there are many of these objects so they cannot be simply a bean
        this.config = new Config();
        this.config.getTopics().add("example-topic");
        this.config.setDltTopicSuffix("-d");
        this.config.setAttempts(4);
        this.config.setBackOffDelay(Duration.ofMillis(100));
    }

    @RetryableTopic(
        backOff = @BackOff(delayString = "#{__listener.config.backOffDelay.toMillis()}"),
        attempts = "#{__listener.config.attempts}",
        dltTopicSuffix = "#{__listener.config.dltTopicSuffix}")
    @KafkaListener(topics = "#{__listener.config.topics})
    public void onMessage(final ConsumerRecord<String, String> consumerRecord) {
        ...
    }

    public Config getConfig() {
        return config;
    }

    public static class Config {

        private final Set<String> topics = new HashSet<>();

        private String dltTopicSuffix;

        private Integer attempts;

        private Duration backOffDelay;

        public Set<String> getTopics() {
            return topics;
        }

        public String getDltTopicSuffix() {
            return dltTopicSuffix;
        }

        public void setDltTopicSuffix(final String dltTopicSuffix) {
            this.dltTopicSuffix = dltTopicSuffix;
        }

        public Integer getAttempts() {
            return attempts;
        }

        public void setAttempts(final Integer attempts) {
            this.attempts = attempts;
        }

        public Duration getBackOffDelay() {
            return backOffDelay;
        }

        public void setBackOffDelay(final Duration backOffDelay) {
            this.backOffDelay = backOffDelay;
        }
    }
}

Expected behavior

The __listener placeholder should be recognized also by the @BackOff annotation, the delayString should be evaluated correctly and the application should start.

Note

This worked in Spring Boot 3 with the old @Backoff annotation from org.springframework.retry.annotation.Backoff. The following configuration before migration to Spring Boot 4 worked:

@RetryableTopic(
    backoff = @Backoff(delayExpression = "#{__listener.config.backOffDelay.toMillis()}"),
    attempts = "#{__listener.config.attempts}",
    dltTopicSuffix = "#{__listener.config.dltTopicSuffix}")
@KafkaListener(topics = "#{__listener.config.topics}")

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions