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

TAKETODAY / today-infrastructure / 18154768944

01 Oct 2025 07:26AM UTC coverage: 81.882% (-0.005%) from 81.887%
18154768944

push

github

web-flow
Merge pull request #290 from TAKETODAY/dev/jspecify

jspecify

59788 of 78013 branches covered (76.64%)

Branch coverage included in aggregate %.

141239 of 167496 relevant lines covered (84.32%)

3.6 hits per line

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

86.09
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.jspecify.annotations.Nullable;
21
import org.reactivestreams.Subscriber;
22
import org.reactivestreams.Subscription;
23

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

37
import infra.aop.support.AopUtils;
38
import infra.context.ApplicationContext;
39
import infra.context.ApplicationEvent;
40
import infra.context.PayloadApplicationEvent;
41
import infra.context.expression.AnnotatedElementKey;
42
import infra.core.BridgeMethodResolver;
43
import infra.core.Ordered;
44
import infra.core.ReactiveAdapter;
45
import infra.core.ReactiveAdapterRegistry;
46
import infra.core.ReactiveStreams;
47
import infra.core.ResolvableType;
48
import infra.core.annotation.MergedAnnotation;
49
import infra.core.annotation.MergedAnnotations;
50
import infra.core.annotation.MergedAnnotations.SearchStrategy;
51
import infra.core.annotation.Order;
52
import infra.lang.Assert;
53
import infra.lang.Constant;
54
import infra.lang.Contract;
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
  @Nullable
116
  private ApplicationContext context;
117

118
  @Nullable
119
  private EventExpressionEvaluator evaluator;
120

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

439
  private void publishEvent(@Nullable Object event) {
440
    if (event != null) {
2✔
441
      Assert.state(context != null, "No ApplicationContext set");
7!
442
      context.publishEvent(event);
4✔
443
    }
444
  }
1✔
445

446
  protected void handleAsyncError(Throwable t) {
447
    log.error("Unexpected error occurred in asynchronous listener", t);
×
448
  }
×
449

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

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

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

496
  /**
497
   * Inner class to avoid a hard dependency on the Reactive Streams API at runtime.
498
   */
499
  private static final class ReactiveDelegate {
500

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

511
  /**
512
   * Reactive Streams Subscriber for publishing follow-up events.
513
   */
514
  private record EventPublicationSubscriber(ApplicationListenerMethodAdapter listener)
6✔
515
          implements Subscriber<Object> {
516

517
    @Override
518
    public void onSubscribe(Subscription s) {
519
      s.request(Integer.MAX_VALUE);
3✔
520
    }
1✔
521

522
    @Override
523
    public void onNext(Object o) {
524
      listener.publishEvents(o);
4✔
525
    }
1✔
526

527
    @Override
528
    public void onError(Throwable t) {
529
      listener.handleAsyncError(t);
×
530
    }
×
531

532
    @Override
533
    public void onComplete() { }
1✔
534
  }
535

536
}
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