• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TAKETODAY / today-infrastructure / 16239315637

12 Jul 2025 03:11PM UTC coverage: 81.778% (+0.005%) from 81.773%
16239315637

push

github

TAKETODAY
:sparkles: Add HttpRequestValues.Processor

59436 of 77625 branches covered (76.57%)

Branch coverage included in aggregate %.

140743 of 167158 relevant lines covered (84.2%)

3.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.24
today-context/src/main/java/infra/context/event/ApplicationListenerMethodAdapter.java
1
/*
2
 * Copyright 2017 - 2025 the original author or authors.
3
 *
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see [https://www.gnu.org/licenses/]
16
 */
17

18
package infra.context.event;
19

20
import org.reactivestreams.Subscriber;
21
import org.reactivestreams.Subscription;
22

23
import java.lang.reflect.InaccessibleObjectException;
24
import java.lang.reflect.InvocationTargetException;
25
import java.lang.reflect.Method;
26
import java.lang.reflect.Proxy;
27
import java.lang.reflect.UndeclaredThrowableException;
28
import java.util.ArrayList;
29
import java.util.Collection;
30
import java.util.Collections;
31
import java.util.List;
32
import java.util.Objects;
33
import java.util.StringJoiner;
34
import java.util.concurrent.CompletionStage;
35

36
import infra.aop.support.AopUtils;
37
import infra.context.ApplicationContext;
38
import infra.context.ApplicationEvent;
39
import infra.context.PayloadApplicationEvent;
40
import infra.context.expression.AnnotatedElementKey;
41
import infra.core.BridgeMethodResolver;
42
import infra.core.Ordered;
43
import infra.core.ReactiveAdapter;
44
import infra.core.ReactiveAdapterRegistry;
45
import infra.core.ReactiveStreams;
46
import infra.core.ResolvableType;
47
import infra.core.annotation.MergedAnnotation;
48
import infra.core.annotation.MergedAnnotations;
49
import infra.core.annotation.MergedAnnotations.SearchStrategy;
50
import infra.core.annotation.Order;
51
import infra.lang.Assert;
52
import infra.lang.Constant;
53
import infra.lang.NonNull;
54
import infra.lang.Nullable;
55
import infra.logging.Logger;
56
import infra.logging.LoggerFactory;
57
import infra.util.ClassUtils;
58
import infra.util.ObjectUtils;
59
import infra.util.ReflectionUtils;
60
import infra.util.StringUtils;
61
import infra.util.concurrent.Future;
62
import infra.util.concurrent.FutureListener;
63

64
/**
65
 * {@link GenericApplicationListener} adapter that delegates the processing of
66
 * an event to an {@link EventListener} annotated method.
67
 *
68
 * <p>Delegates to {@link #processEvent(ApplicationEvent)} to give subclasses
69
 * a chance to deviate from the default. Unwraps the content of a
70
 * {@link PayloadApplicationEvent} if necessary to allow a method declaration
71
 * to define any arbitrary event type. If a condition is defined, it is
72
 * evaluated prior to invoking the underlying method.
73
 *
74
 * @author Stephane Nicoll
75
 * @author Juergen Hoeller
76
 * @author Sam Brannen
77
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
78
 * @since 4.0 2021/11/5 11:51
79
 */
80
public class ApplicationListenerMethodAdapter implements GenericApplicationListener, Ordered, FutureListener<Future<?>> {
81

82
  private static final Logger log = LoggerFactory.getLogger(ApplicationListenerMethodAdapter.class);
4✔
83

84
  private final Method method;
85

86
  private final Method targetMethod;
87

88
  /**
89
   * <p>Matches the {@code condition} attribute of the {@link EventListener}
90
   * annotation or any matching attribute on a composed annotation that
91
   * is meta-annotated with {@code @EventListener}.
92
   */
93
  @Nullable
94
  protected final String condition;
95

96
  private final String beanName;
97

98
  private final int order;
99

100
  /**
101
   * Whether default execution is applicable for the target listener.
102
   *
103
   * @see #onApplicationEvent
104
   * @see EventListener#defaultExecution()
105
   */
106
  protected final boolean defaultExecution;
107

108
  private final List<ResolvableType> declaredEventTypes;
109

110
  private final AnnotatedElementKey methodKey;
111

112
  @Nullable
113
  private volatile String listenerId;
114

115
  private ApplicationContext context;
116

117
  @Nullable
118
  private EventExpressionEvaluator evaluator;
119

120
  /**
121
   * Construct a new MethodApplicationListener.
122
   *
123
   * @param beanName the name of the bean to invoke the listener method on
124
   * @param targetClass the target class that the method is declared on
125
   * @param method the listener method to invoke
126
   */
127
  public ApplicationListenerMethodAdapter(String beanName, Class<?> targetClass, Method method) {
2✔
128
    this.beanName = beanName;
3✔
129
    this.method = BridgeMethodResolver.findBridgedMethod(method);
4✔
130
    this.targetMethod = Proxy.isProxyClass(targetClass)
4✔
131
            ? this.method : AopUtils.getMostSpecificMethod(method, targetClass);
7✔
132
    this.methodKey = new AnnotatedElementKey(this.targetMethod, targetClass);
8✔
133

134
    MergedAnnotations annotations = MergedAnnotations.from(targetMethod, SearchStrategy.TYPE_HIERARCHY);
5✔
135
    MergedAnnotation<EventListener> annotation = annotations.get(EventListener.class);
4✔
136
    this.declaredEventTypes = resolveDeclaredEventTypes(method, annotation);
5✔
137

138
    String condition = annotation.getString("condition");
4✔
139
    if (StringUtils.hasText(condition)) {
3✔
140
      this.condition = condition;
4✔
141
    }
142
    else {
143
      this.condition = null;
3✔
144
    }
145

146
    this.defaultExecution = annotation.getBoolean("defaultExecution");
5✔
147
    if (annotation.isPresent()) {
3!
148
      String id = annotation.getString("id");
4✔
149
      this.listenerId = !id.isEmpty() ? id : null;
8✔
150
    }
151

152
    this.order = Objects.requireNonNullElse(
7✔
153
            annotations.get(Order.class).getValue(int.class), Ordered.LOWEST_PRECEDENCE);
6✔
154
  }
1✔
155

156
  protected void init(ApplicationContext context, @Nullable EventExpressionEvaluator evaluator) {
157
    this.context = context;
3✔
158
    this.evaluator = evaluator;
3✔
159
  }
1✔
160

161
  private static List<ResolvableType> resolveDeclaredEventTypes(Method method, @Nullable MergedAnnotation<EventListener> ann) {
162
    int count = method.getParameterCount();
3✔
163
    if (count > 1) {
3✔
164
      throw new IllegalStateException(
7✔
165
              "Maximum one parameter is allowed for event listener method: " + method);
166
    }
167
    if (ann != null) {
2!
168
      Class<?>[] classes = ann.getClassArray("event");
4✔
169
      if (classes.length > 0) {
3✔
170
        ArrayList<ResolvableType> types = new ArrayList<>(classes.length);
6✔
171
        for (Class<?> eventType : classes) {
16✔
172
          types.add(ResolvableType.forClass(eventType));
5✔
173
        }
174
        return types;
2✔
175
      }
176
    }
177

178
    if (count == 0) {
2✔
179
      throw new IllegalStateException(
7✔
180
              "Event parameter is mandatory for event listener method: " + method);
181
    }
182
    return Collections.singletonList(ResolvableType.forParameter(method, 0));
5✔
183
  }
184

185
  @Override
186
  public int getOrder() {
187
    return order;
3✔
188
  }
189

190
  /**
191
   * Return the target bean instance to use.
192
   */
193
  @Nullable
194
  protected Object getTargetBean() {
195
    Assert.state(context != null, "No ApplicationContext set");
7!
196
    return context.getBean(beanName);
6✔
197
  }
198

199
  /**
200
   * Return the target listener method.
201
   */
202
  protected Method getTargetMethod() {
203
    return this.targetMethod;
3✔
204
  }
205

206
  @Override
207
  public boolean supportsEventType(ResolvableType eventType) {
208
    for (ResolvableType declaredEventType : this.declaredEventTypes) {
11✔
209
      if (eventType.hasUnresolvableGenerics() ?
4✔
210
              declaredEventType.toClass().isAssignableFrom(eventType.toClass()) :
8✔
211
              declaredEventType.isAssignableFrom(eventType)) {
2✔
212
        return true;
2✔
213
      }
214
      if (PayloadApplicationEvent.class.isAssignableFrom(eventType.toClass())) {
5✔
215
        ResolvableType payloadType = eventType.as(PayloadApplicationEvent.class).getGeneric();
7✔
216
        if (declaredEventType.isAssignableFrom(payloadType)) {
4✔
217
          return true;
2✔
218
        }
219
        if (payloadType.resolve() == null) {
3✔
220
          // Always accept such event when the type is erased
221
          return true;
2✔
222
        }
223
      }
224
    }
1✔
225
    return false;
2✔
226
  }
227

228
  @Override
229
  public boolean supportsSourceType(@Nullable Class<?> sourceType) {
230
    return true;
2✔
231
  }
232

233
  /**
234
   * Process the specified {@link Object}, checking if the condition
235
   * matches and handling a non-null result, if any.
236
   */
237
  @Override
238
  public void onApplicationEvent(ApplicationEvent event) { // any event type
239
    if (defaultExecution) {
3✔
240
      processEvent(event);
3✔
241
    }
242
  }
1✔
243

244
  /**
245
   * Process the specified {@link ApplicationEvent}, checking if the condition
246
   * matches and handling a non-null result, if any.
247
   *
248
   * @param event the event to process through the listener method
249
   */
250
  public void processEvent(ApplicationEvent event) {
251
    Object[] args = resolveArguments(event);
4✔
252
    if (shouldInvoke(event, args)) {
5✔
253
      Object result = doInvoke(args);
4✔
254
      if (result != null) {
2✔
255
        handleResult(result);
4✔
256
      }
257
      else {
258
        log.trace("No result object given - no result to handle");
3✔
259
      }
260
    }
261
  }
1✔
262

263
  private boolean shouldInvoke(Object event, @Nullable Object[] args) {
264
    if (args == null) {
2✔
265
      return false;
2✔
266
    }
267
    if (condition != null) {
3✔
268
      Assert.notNull(this.evaluator, "EventExpressionEvaluator is required");
4✔
269
      return this.evaluator.condition(
12✔
270
              condition, event, this.targetMethod, this.methodKey, args);
271
    }
272
    return true;
2✔
273
  }
274

275
  /**
276
   * Resolve the method arguments to use for the specified {@link ApplicationEvent}.
277
   * <p>These arguments will be used to invoke the method handled by this instance.
278
   * Can return {@code null} to indicate that no suitable arguments could be resolved
279
   * and therefore the method should not be invoked at all for the specified event.
280
   */
281
  @Nullable
282
  protected Object[] resolveArguments(ApplicationEvent event) {
283
    ResolvableType declaredEventType = getResolvableType(event);
4✔
284
    if (declaredEventType == null) {
2✔
285
      return null;
2✔
286
    }
287
    if (method.getParameterCount() == 0) {
4✔
288
      return Constant.EMPTY_OBJECTS;
2✔
289
    }
290
    Class<?> declaredEventClass = declaredEventType.toClass();
3✔
291
    if (!ApplicationEvent.class.isAssignableFrom(declaredEventClass)
7!
292
            && event instanceof PayloadApplicationEvent) {
293
      Object payload = ((PayloadApplicationEvent<?>) event).getPayload();
4✔
294
      if (declaredEventClass.isInstance(payload)) {
4✔
295
        return new Object[] { payload };
7✔
296
      }
297
    }
298
    return new Object[] { event };
7✔
299
  }
300

301
  @Nullable
302
  private ResolvableType getResolvableType(ApplicationEvent event) {
303
    ResolvableType payloadType = null;
2✔
304
    if (event instanceof PayloadApplicationEvent<?> payloadEvent) {
6✔
305
      ResolvableType eventType = payloadEvent.getResolvableType();
3✔
306
      if (eventType != null) {
2!
307
        payloadType = eventType.as(PayloadApplicationEvent.class).getGeneric();
7✔
308
      }
309
    }
310
    for (ResolvableType declaredEventType : this.declaredEventTypes) {
11✔
311
      Class<?> eventClass = declaredEventType.toClass();
3✔
312
      if (!ApplicationEvent.class.isAssignableFrom(eventClass) &&
8!
313
              payloadType != null && declaredEventType.isAssignableFrom(payloadType)) {
2✔
314
        return declaredEventType;
2✔
315
      }
316
      if (eventClass.isInstance(event)) {
4✔
317
        return declaredEventType;
2✔
318
      }
319
    }
1✔
320
    return null;
2✔
321
  }
322

323
  /**
324
   * Invoke the event listener method with the given argument values.
325
   */
326
  @Nullable
327
  protected Object doInvoke(Object[] args) {
328
    Object bean = getTargetBean();
3✔
329
    // Detect package-protected NullBean instance through equals(null) check
330
    if (bean == null) {
2✔
331
      return null;
2✔
332
    }
333
    try {
334
      ReflectionUtils.makeAccessible(method);
4✔
335
      return method.invoke(bean, args);
6✔
336
    }
337
    catch (IllegalArgumentException ex) {
1✔
338
      assertTargetBean(method, bean, args);
×
339
      throw new IllegalStateException(getInvocationErrorMessage(bean, ex.getMessage(), args), ex);
×
340
    }
341
    catch (IllegalAccessException | InaccessibleObjectException ex) {
×
342
      throw new IllegalStateException(getInvocationErrorMessage(bean, ex.getMessage(), args), ex);
×
343
    }
344
    catch (InvocationTargetException ex) {
1✔
345
      // Throw underlying exception
346
      Throwable targetException = ex.getTargetException();
3✔
347
      if (targetException instanceof RuntimeException) {
3✔
348
        throw (RuntimeException) targetException;
3✔
349
      }
350
      else {
351
        String msg = getInvocationErrorMessage(bean, "Failed to invoke event listener method", args);
6✔
352
        throw new UndeclaredThrowableException(targetException, msg);
6✔
353
      }
354
    }
355
  }
356

357
  @Override
358
  @NonNull
359
  public String getListenerId() {
360
    String id = this.listenerId;
3✔
361
    if (id == null) {
2✔
362
      id = getDefaultListenerId();
3✔
363
      this.listenerId = id;
3✔
364
    }
365
    return id;
2✔
366
  }
367

368
  /**
369
   * Determine the default id for the target listener, to be applied in case of
370
   * no {@link EventListener#id() annotation-specified id value}.
371
   * <p>The default implementation builds a method name with parameter types.
372
   *
373
   * @see #getListenerId()
374
   */
375
  protected String getDefaultListenerId() {
376
    Method method = getTargetMethod();
3✔
377
    StringJoiner sj = new StringJoiner(",", "(", ")");
7✔
378
    for (Class<?> paramType : method.getParameterTypes()) {
17✔
379
      sj.add(paramType.getName());
5✔
380
    }
381
    return ClassUtils.getQualifiedMethodName(method) + sj;
6✔
382
  }
383

384
  @SuppressWarnings({ "rawtypes", "unchecked" })
385
  protected void handleResult(Object result) {
386
    if (ReactiveStreams.isPresent && ReactiveDelegate.subscribeToPublisher(this, result)) {
6!
387
      if (log.isTraceEnabled()) {
3!
388
        log.trace("Adapted to reactive result: {}", result);
×
389
      }
390
    }
391
    else if (result instanceof CompletionStage<?> stage) {
3!
392
      stage.whenComplete((event, ex) -> {
×
393
        if (ex != null) {
×
394
          handleAsyncError(ex);
×
395
        }
396
        else if (event != null) {
×
397
          publishEvents(event);
×
398
        }
399
      });
×
400
    }
401
    else if (result instanceof Future d) {
3!
402
      d.onCompleted(this);
×
403
    }
404
    else {
405
      publishEvents(result);
3✔
406
    }
407
  }
1✔
408

409
  @Override
410
  public void operationComplete(Future<?> future) {
411
    Throwable cause = future.getCause();
×
412
    if (cause != null) {
×
413
      handleAsyncError(cause);
×
414
    }
415
    else {
416
      publishEvents(future.obtain());
×
417
    }
418
  }
×
419

420
  private void publishEvents(Object result) {
421
    if (result.getClass().isArray()) {
4✔
422
      Object[] events = ObjectUtils.toObjectArray(result);
3✔
423
      for (Object event : events) {
16✔
424
        publishEvent(event);
3✔
425
      }
426
    }
1✔
427
    else if (result instanceof Collection<?> events) {
6✔
428
      for (Object event : events) {
9✔
429
        publishEvent(event);
3✔
430
      }
2✔
431
    }
432
    else {
433
      publishEvent(result);
3✔
434
    }
435
  }
1✔
436

437
  private void publishEvent(@Nullable Object event) {
438
    if (event != null) {
2✔
439
      context.publishEvent(event);
4✔
440
    }
441
  }
1✔
442

443
  protected void handleAsyncError(Throwable t) {
444
    log.error("Unexpected error occurred in asynchronous listener", t);
×
445
  }
×
446

447
  /**
448
   * Add additional details such as the bean type and method signature to
449
   * the given error message.
450
   *
451
   * @param message error message to append the HandlerMethod details to
452
   */
453
  protected String getDetailedErrorMessage(Object bean, String message) {
454
    return "%s\nHandlerMethod details: \nBean [%s]\nMethod [%s]\n"
11✔
455
            .formatted(message, bean.getClass().getName(), this.targetMethod.toGenericString());
10✔
456
  }
457

458
  /**
459
   * Assert that the target bean class is an instance of the class where the given
460
   * method is declared. In some cases the actual bean instance at event-
461
   * processing time may be a JDK dynamic proxy (lazy initialization, prototype
462
   * beans, and others). Event listener beans that require proxying should prefer
463
   * class-based proxy mechanisms.
464
   */
465
  private void assertTargetBean(Method method, Object targetBean, Object[] args) {
466
    Class<?> methodDeclaringClass = method.getDeclaringClass();
3✔
467
    Class<?> targetBeanClass = targetBean.getClass();
3✔
468
    if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) {
4!
469
      String msg = ("The event listener method class '%s' is not an instance of the actual bean class '%s'. " +
6✔
470
              "If the bean requires proxying (e.g. due to @Transactional), please use class-based proxying.")
471
              .formatted(methodDeclaringClass.getName(), targetBeanClass.getName());
9✔
472
      throw new IllegalStateException(getInvocationErrorMessage(targetBean, msg, args));
9✔
473
    }
474
  }
×
475

476
  private String getInvocationErrorMessage(Object bean, String message, Object[] resolvedArgs) {
477
    StringBuilder sb = new StringBuilder(getDetailedErrorMessage(bean, message));
8✔
478
    sb.append("Resolved arguments: \n");
4✔
479
    for (int i = 0; i < resolvedArgs.length; i++) {
8✔
480
      sb.append('[').append(i).append("] ");
8✔
481
      if (resolvedArgs[i] == null) {
4!
482
        sb.append("[null] \n");
×
483
      }
484
      else {
485
        sb.append("[type=").append(resolvedArgs[i].getClass().getName()).append("] ");
12✔
486
        sb.append("[value=").append(resolvedArgs[i]).append("]\n");
10✔
487
      }
488
    }
489
    return sb.toString();
3✔
490
  }
491

492
  /**
493
   * Inner class to avoid a hard dependency on the Reactive Streams API at runtime.
494
   */
495
  private static final class ReactiveDelegate {
496

497
    public static boolean subscribeToPublisher(ApplicationListenerMethodAdapter listener, Object result) {
498
      ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(result.getClass());
5✔
499
      if (adapter != null) {
2✔
500
        adapter.toPublisher(result).subscribe(new EventPublicationSubscriber(listener));
8✔
501
        return true;
2✔
502
      }
503
      return false;
2✔
504
    }
505
  }
506

507
  /**
508
   * Reactive Streams Subscriber for publishing follow-up events.
509
   */
510
  private record EventPublicationSubscriber(ApplicationListenerMethodAdapter listener)
6✔
511
          implements Subscriber<Object> {
512

513
    @Override
514
    public void onSubscribe(Subscription s) {
515
      s.request(Integer.MAX_VALUE);
3✔
516
    }
1✔
517

518
    @Override
519
    public void onNext(Object o) {
520
      listener.publishEvents(o);
4✔
521
    }
1✔
522

523
    @Override
524
    public void onError(Throwable t) {
525
      listener.handleAsyncError(t);
×
526
    }
×
527

528
    @Override
529
    public void onComplete() { }
1✔
530
  }
531

532
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc