Skip to content

Commit ad1ae60

Browse files
authored
GH-1817: Allow Annotation Attribute Modification
Resolves #1817 * Fix flaky reactor test - add a delay to prevent canceling the consumer before offsets sent. * Simplify for developers; support multiple enhancers; add coverage for repeated listeners. * Docs for new changes. * Doc that enhancer bean definitions must be static; add test.
1 parent cb4ad95 commit ad1ae60

File tree

7 files changed

+216
-22
lines changed

7 files changed

+216
-22
lines changed

samples/sample-04/src/main/java/com/example/Application.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public static void main(String[] args) {
4545
}
4646

4747
@RetryableTopic(attempts = "5", backoff = @Backoff(delay = 2_000, maxDelay = 10_000, multiplier = 2))
48-
@KafkaListener(id = "fooGroup", topics = "topic4")
48+
@KafkaListener(id = "fooGroup", topics = "topic4", clientIdPrefix = "test")
4949
public void listen(String in, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
5050
@Header(KafkaHeaders.OFFSET) long offset) {
5151

Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
logging.level.root=off
2+
logging.level.org.apache.kafka=info
23
logging.level.com.example=info
34
#logging.level.org.springframework.kafka=error

spring-kafka-docs/src/main/asciidoc/kafka.adoc

+29
Original file line numberDiff line numberDiff line change
@@ -1863,6 +1863,35 @@ void listen(Object in, @Header(KafkaHeaders.RECORD_METADATA) ConsumerRecordMetad
18631863
----
18641864
====
18651865

1866+
[[kafkalistener-attrs]]
1867+
===== `@KafkaListener` Attribute Modification
1868+
1869+
Starting with version 2.7.2, you can now programmatically modify annotation attributes before the container is created.
1870+
To do so, add one or more `KafkaListenerAnnotationBeanPostProcessor.AnnotationEnhancer` to the application context.
1871+
`AnnotationEnhancer` is a `BiFunction<Map<String, Object>, AnnotatedElement, Map<String, Object>` and must return a a map of attributes.
1872+
The attribute values can contain SpEL and/or property placeholders; the enhancer is called before any resolution is performed.
1873+
If more than one enhancer is present, and they implement `Ordered`, they will be invoked in order.
1874+
1875+
IMPORTANT: `AnnotationEnhancer` bean definitions must be declared `static` because they are required very early in the application context's lifecycle.
1876+
1877+
An example follows:
1878+
1879+
====
1880+
[source, java]
1881+
----
1882+
@Bean
1883+
public static AnnotationEnhancer groupIdEnhancer() {
1884+
return (attrs, element) -> {
1885+
attrs.put("groupId", attrs.get("id") + "." + (element instanceof Class
1886+
? ((Class<?>) element).getSimpleName()
1887+
: ((Method) element).getDeclaringClass().getSimpleName()
1888+
+ "." + ((Method) element).getName()));
1889+
return attrs;
1890+
};
1891+
}
1892+
----
1893+
====
1894+
18661895
[[kafkalistener-lifecycle]]
18671896
===== `@KafkaListener` Lifecycle Management
18681897

spring-kafka-docs/src/main/asciidoc/whats-new.adoc

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ You can now set the `rawRecordHeader` property on the `MessagingMessageConverter
4848
This is useful, for example, if you wish to use a `DeadLetterPublishingRecoverer` in a listener error handler.
4949
See <<listener-error-handlers>> for more information.
5050

51+
You can now modify `@KafkaListener` annotations during application initialization.
52+
See <<kafkalistener-attrs>> for more information.
53+
5154
[[x27-dlt]]
5255
==== `DeadLetterPublishingRecover` Changes
5356

spring-kafka/src/main/java/org/springframework/kafka/annotation/KafkaListenerAnnotationBeanPostProcessor.java

+81-11
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.io.IOException;
2020
import java.io.StringReader;
21+
import java.lang.reflect.AnnotatedElement;
2122
import java.lang.reflect.Method;
2223
import java.nio.charset.Charset;
2324
import java.nio.charset.StandardCharsets;
@@ -33,6 +34,7 @@
3334
import java.util.Set;
3435
import java.util.concurrent.ConcurrentHashMap;
3536
import java.util.concurrent.atomic.AtomicInteger;
37+
import java.util.function.BiFunction;
3638
import java.util.regex.Pattern;
3739
import java.util.stream.Collectors;
3840
import java.util.stream.Stream;
@@ -43,8 +45,8 @@
4345
import org.springframework.aop.support.AopUtils;
4446
import org.springframework.beans.BeansException;
4547
import org.springframework.beans.factory.BeanFactory;
46-
import org.springframework.beans.factory.BeanFactoryAware;
4748
import org.springframework.beans.factory.BeanInitializationException;
49+
import org.springframework.beans.factory.InitializingBean;
4850
import org.springframework.beans.factory.ListableBeanFactory;
4951
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
5052
import org.springframework.beans.factory.ObjectFactory;
@@ -56,9 +58,13 @@
5658
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
5759
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
5860
import org.springframework.beans.factory.config.Scope;
61+
import org.springframework.context.ApplicationContext;
62+
import org.springframework.context.ApplicationContextAware;
63+
import org.springframework.context.ConfigurableApplicationContext;
5964
import org.springframework.context.expression.StandardBeanExpressionResolver;
6065
import org.springframework.core.MethodIntrospector;
6166
import org.springframework.core.MethodParameter;
67+
import org.springframework.core.OrderComparator;
6268
import org.springframework.core.Ordered;
6369
import org.springframework.core.annotation.AnnotatedElementUtils;
6470
import org.springframework.core.annotation.AnnotationUtils;
@@ -134,7 +140,7 @@
134140
* @see MethodKafkaListenerEndpoint
135141
*/
136142
public class KafkaListenerAnnotationBeanPostProcessor<K, V>
137-
implements BeanPostProcessor, Ordered, BeanFactoryAware, SmartInitializingSingleton {
143+
implements BeanPostProcessor, Ordered, ApplicationContextAware, InitializingBean, SmartInitializingSingleton {
138144

139145
private static final String GENERATED_ID_PREFIX = "org.springframework.kafka.KafkaListenerEndpointContainer#";
140146

@@ -149,25 +155,29 @@ public class KafkaListenerAnnotationBeanPostProcessor<K, V>
149155

150156
private final ListenerScope listenerScope = new ListenerScope();
151157

152-
private KafkaListenerEndpointRegistry endpointRegistry;
153-
154-
private String defaultContainerFactoryBeanName = DEFAULT_KAFKA_LISTENER_CONTAINER_FACTORY_BEAN_NAME;
155-
156-
private BeanFactory beanFactory;
157-
158158
private final KafkaHandlerMethodFactoryAdapter messageHandlerMethodFactory =
159159
new KafkaHandlerMethodFactoryAdapter();
160160

161161
private final KafkaListenerEndpointRegistrar registrar = new KafkaListenerEndpointRegistrar();
162162

163163
private final AtomicInteger counter = new AtomicInteger();
164164

165+
private KafkaListenerEndpointRegistry endpointRegistry;
166+
167+
private String defaultContainerFactoryBeanName = DEFAULT_KAFKA_LISTENER_CONTAINER_FACTORY_BEAN_NAME;
168+
169+
private ApplicationContext applicationContext;
170+
171+
private BeanFactory beanFactory;
172+
165173
private BeanExpressionResolver resolver = new StandardBeanExpressionResolver();
166174

167175
private BeanExpressionContext expressionContext;
168176

169177
private Charset charset = StandardCharsets.UTF_8;
170178

179+
private AnnotationEnhancer enhancer;
180+
171181
@Override
172182
public int getOrder() {
173183
return LOWEST_PRECEDENCE;
@@ -213,13 +223,23 @@ public MessageHandlerMethodFactory getMessageHandlerMethodFactory() {
213223
return this.messageHandlerMethodFactory;
214224
}
215225

226+
@Override
227+
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
228+
this.applicationContext = applicationContext;
229+
if (applicationContext instanceof ConfigurableApplicationContext) {
230+
setBeanFactory(((ConfigurableApplicationContext) applicationContext).getBeanFactory());
231+
}
232+
else {
233+
setBeanFactory(applicationContext);
234+
}
235+
}
236+
216237
/**
217238
* Making a {@link BeanFactory} available is optional; if not set,
218239
* {@link KafkaListenerConfigurer} beans won't get autodetected and an
219240
* {@link #setEndpointRegistry endpoint registry} has to be explicitly configured.
220241
* @param beanFactory the {@link BeanFactory} to be used.
221242
*/
222-
@Override
223243
public void setBeanFactory(BeanFactory beanFactory) {
224244
this.beanFactory = beanFactory;
225245
if (beanFactory instanceof ConfigurableListableBeanFactory) {
@@ -240,6 +260,11 @@ public void setCharset(Charset charset) {
240260
this.charset = charset;
241261
}
242262

263+
@Override
264+
public void afterPropertiesSet() throws Exception {
265+
buildEnhancer();
266+
}
267+
243268
@Override
244269
public void afterSingletonsInstantiated() {
245270
this.registrar.setBeanFactory(this.beanFactory);
@@ -280,6 +305,25 @@ public void afterSingletonsInstantiated() {
280305
this.registrar.afterPropertiesSet();
281306
}
282307

308+
private void buildEnhancer() {
309+
if (this.applicationContext != null) {
310+
Map<String, AnnotationEnhancer> enhancersMap =
311+
this.applicationContext.getBeansOfType(AnnotationEnhancer.class, false, false);
312+
if (enhancersMap.size() > 0) {
313+
List<AnnotationEnhancer> enhancers = enhancersMap.values()
314+
.stream()
315+
.sorted(new OrderComparator())
316+
.collect(Collectors.toList());
317+
this.enhancer = (attrs, element) -> {
318+
Map<String, Object> newAttrs = attrs;
319+
for (AnnotationEnhancer enhancer : enhancers) {
320+
newAttrs = enhancer.apply(newAttrs, element);
321+
}
322+
return attrs;
323+
};
324+
}
325+
}
326+
}
283327

284328
@Override
285329
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
@@ -333,11 +377,14 @@ private Collection<KafkaListener> findListenerAnnotations(Class<?> clazz) {
333377
Set<KafkaListener> listeners = new HashSet<>();
334378
KafkaListener ann = AnnotatedElementUtils.findMergedAnnotation(clazz, KafkaListener.class);
335379
if (ann != null) {
380+
ann = enhance(clazz, ann);
336381
listeners.add(ann);
337382
}
338383
KafkaListeners anns = AnnotationUtils.findAnnotation(clazz, KafkaListeners.class);
339384
if (anns != null) {
340-
listeners.addAll(Arrays.asList(anns.value()));
385+
listeners.addAll(Arrays.stream(anns.value())
386+
.map(anno -> enhance(clazz, anno))
387+
.collect(Collectors.toList()));
341388
}
342389
return listeners;
343390
}
@@ -349,15 +396,28 @@ private Set<KafkaListener> findListenerAnnotations(Method method) {
349396
Set<KafkaListener> listeners = new HashSet<>();
350397
KafkaListener ann = AnnotatedElementUtils.findMergedAnnotation(method, KafkaListener.class);
351398
if (ann != null) {
399+
ann = enhance(method, ann);
352400
listeners.add(ann);
353401
}
354402
KafkaListeners anns = AnnotationUtils.findAnnotation(method, KafkaListeners.class);
355403
if (anns != null) {
356-
listeners.addAll(Arrays.asList(anns.value()));
404+
listeners.addAll(Arrays.stream(anns.value())
405+
.map(anno -> enhance(method, anno))
406+
.collect(Collectors.toList()));
357407
}
358408
return listeners;
359409
}
360410

411+
private KafkaListener enhance(AnnotatedElement element, KafkaListener ann) {
412+
if (this.enhancer == null) {
413+
return ann;
414+
}
415+
else {
416+
return AnnotationUtils.synthesizeAnnotation(
417+
this.enhancer.apply(AnnotationUtils.getAnnotationAttributes(ann), element), KafkaListener.class, null);
418+
}
419+
}
420+
361421
private void processMultiMethodListeners(Collection<KafkaListener> classLevelListeners, List<Method> multiMethods,
362422
Object bean, String beanName) {
363423

@@ -1070,4 +1130,14 @@ protected boolean isEmptyPayload(Object payload) {
10701130

10711131
}
10721132

1133+
/**
1134+
* Post processes each set of annotation attributes.
1135+
*
1136+
* @since 2.7.2
1137+
*
1138+
*/
1139+
public interface AnnotationEnhancer extends BiFunction<Map<String, Object>, AnnotatedElement, Map<String, Object>> {
1140+
1141+
}
1142+
10731143
}

0 commit comments

Comments
 (0)