Add core-events module: @EventListener, @TransactionalEventListener phases and async listeners on Boot 4.1
Thirteen captured transcripts: a listener is a blocking method call, ordering and chaining, SpEL conditions, generic-event erasure, every transaction phase on commit and rollback, what an AFTER_COMMIT listener can write, which exceptions reach the publisher, async listeners on platform and virtual threads, and the annotations behind Spring Modulith's @ApplicationModuleListener. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Uu7q8vPeREyT4218EJPzz1
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# OrderService publishes an OrderPlaced; the listeners run before publishEvent returns, on the same thread
|
||||
|
||||
OrderService.place: publishing on main
|
||||
AuditListener.audit: received OrderPlaced[id=A-1, amount=42] on main
|
||||
AuditListener.counted: no parameter, still called
|
||||
OrderService.place: publishEvent returned
|
||||
@@ -0,0 +1,18 @@
|
||||
# Listener order, an event returned from a listener, and a listener that throws
|
||||
|
||||
|
||||
--- all listeners succeed ---
|
||||
earliest (@Order(-10), a different bean)
|
||||
receipt (@Order(0)) returns a ReceiptIssued
|
||||
onReceipt received ReceiptIssued[orderId=A-1]
|
||||
first (@Order(1))
|
||||
second (@Order(2))
|
||||
unordered (no @Order)
|
||||
|
||||
--- the @Order(2) listener throws ---
|
||||
earliest (@Order(-10), a different bean)
|
||||
receipt (@Order(0)) returns a ReceiptIssued
|
||||
onReceipt received ReceiptIssued[orderId=A-2]
|
||||
first (@Order(1))
|
||||
second (@Order(2))
|
||||
publishEvent threw: IllegalStateException: second listener failed
|
||||
@@ -0,0 +1,21 @@
|
||||
# @EventListener(condition = ...) evaluated against the event, and a condition that names a property that does not exist
|
||||
|
||||
|
||||
--- OrderPlaced(VIP-1, 150) ---
|
||||
large (amount >= 100)
|
||||
vip (id starts with VIP)
|
||||
both (large and VIP)
|
||||
|
||||
--- OrderPlaced(VIP-2, 20) ---
|
||||
vip (id starts with VIP)
|
||||
|
||||
--- OrderPlaced(X-3, 500) ---
|
||||
large (amount >= 100)
|
||||
|
||||
--- OrderPlaced(X-4, 1) ---
|
||||
(no listener ran)
|
||||
|
||||
--- a condition that says #event.amountt ---
|
||||
context started: true
|
||||
publishEvent threw: SpelEvaluationException
|
||||
root cause: EL1008E: Property or field 'amountt' cannot be found on object of type 'com.ankurm.events.badcondition.OrderPlaced' - maybe not public or not valid?
|
||||
@@ -0,0 +1,18 @@
|
||||
# Created<Order> and Created<Customer> published as plain payloads, then the same with ResolvableTypeProvider
|
||||
|
||||
|
||||
--- publishing Created<Order> ---
|
||||
Created<?> listener got a Order
|
||||
publishEvent returned normally
|
||||
|
||||
--- publishing Created<Customer> ---
|
||||
Created<?> listener got a Customer
|
||||
publishEvent returned normally
|
||||
|
||||
--- publishing TypedCreated<Order> ---
|
||||
TypedCreated<Order> listener got a Order
|
||||
publishEvent returned normally
|
||||
|
||||
--- publishing TypedCreated<Customer> ---
|
||||
TypedCreated<Customer> listener got a Customer
|
||||
publishEvent returned normally
|
||||
@@ -0,0 +1,21 @@
|
||||
# The same listeners, one publisher call that commits and one that rolls back
|
||||
|
||||
|
||||
--- place("ok-1", fail = false) ---
|
||||
OrderService.place: row inserted, publishing
|
||||
@EventListener tx active: true row visible to other connections: false
|
||||
OrderService.place: leaving the method, commit follows
|
||||
BEFORE_COMMIT tx active: true row visible to other connections: false
|
||||
AFTER_COMMIT tx active: true row visible to other connections: true
|
||||
AFTER_COMPLETION tx active: true row visible to other connections: true
|
||||
|
||||
--- place("bad-1", fail = true) ---
|
||||
OrderService.place: row inserted, publishing
|
||||
@EventListener tx active: true row visible to other connections: false
|
||||
AFTER_ROLLBACK tx active: true row visible to other connections: false
|
||||
AFTER_COMPLETION tx active: true row visible to other connections: false
|
||||
caller saw: IllegalStateException: payment declined
|
||||
|
||||
--- rows afterwards ---
|
||||
ok-1 rows : 1
|
||||
bad-1 rows: 0
|
||||
@@ -0,0 +1,5 @@
|
||||
# Publishing outside any transaction: which listeners run
|
||||
|
||||
OrderService.placeWithoutTransaction: row inserted (auto-commit), publishing
|
||||
@EventListener tx active: false row visible to other connections: true
|
||||
AFTER_COMMIT or immediately (fallbackExecution = true)
|
||||
@@ -0,0 +1,16 @@
|
||||
# An AFTER_COMMIT listener that inserts a row: three ways to declare it
|
||||
|
||||
|
||||
--- no transaction attribute on the listener ---
|
||||
inside the listener, right after the insert: audit row visible to other connections: false
|
||||
after place() returned, order rows: 1
|
||||
after place() returned, audit rows: 1
|
||||
|
||||
--- @Transactional(propagation = REQUIRES_NEW) on the listener ---
|
||||
order rows: 1
|
||||
audit rows: 1
|
||||
|
||||
--- @Transactional (REQUIRED, the default) on the listener ---
|
||||
context started: false
|
||||
failure chain: BeanInitializationException -> IllegalStateException
|
||||
root cause: @TransactionalEventListener method must not be annotated with @Transactional unless when declared as REQUIRES_NEW or NOT_SUPPORTED: void com.ankurm.events.txwrite.required.AuditRequired.audit(com.ankurm.events.txwrite.shared.OrderPlaced)
|
||||
@@ -0,0 +1,16 @@
|
||||
# Which listener's exception reaches the caller, and whether the order row survives
|
||||
|
||||
|
||||
--- plain listener throws ---
|
||||
caller saw: IllegalStateException: plain listener failed
|
||||
order row survived: false
|
||||
|
||||
--- beforeCommit listener throws ---
|
||||
caller saw: IllegalStateException: BEFORE_COMMIT listener failed
|
||||
order row survived: false
|
||||
|
||||
--- afterCommit listener throws ---
|
||||
caller saw: nothing
|
||||
order row survived: true
|
||||
logged: TransactionSynchronization.afterCompletion threw exception
|
||||
logged: java.lang.IllegalStateException: AFTER_COMMIT listener failed
|
||||
@@ -0,0 +1,15 @@
|
||||
# @Async @EventListener: the publisher moves on first; which thread the listener gets
|
||||
|
||||
|
||||
--- spring.threads.virtual.enabled=false ---
|
||||
publishing on main
|
||||
publishEvent returned, releasing the listener
|
||||
@Async @EventListener: running on platform thread task-1, after publishEvent returned
|
||||
|
||||
--- spring.threads.virtual.enabled=true ---
|
||||
publishing on main
|
||||
publishEvent returned, releasing the listener
|
||||
@Async @EventListener: running on a virtual thread, after publishEvent returned
|
||||
|
||||
--- the same listener in an application without @EnableAsync ---
|
||||
@Async @EventListener: running on main
|
||||
@@ -0,0 +1,5 @@
|
||||
# Two async listeners for an event published inside a transaction
|
||||
|
||||
@Async @EventListener: transaction active on this thread: false; row visible: false
|
||||
AsyncOrderService.place: leaving the method, commit follows
|
||||
@Async @TransactionalEventListener(AFTER_COMMIT): on a virtual thread; row visible: true
|
||||
@@ -0,0 +1,4 @@
|
||||
# An @Async listener that throws: what the publisher sees, and where the exception goes
|
||||
|
||||
publishEvent returned normally
|
||||
logged: Unexpected exception occurred invoking async method: void com.ankurm.events.asyncdemo.AsyncListeners.boom(com.ankurm.events.asyncdemo.Exploding)
|
||||
@@ -0,0 +1,5 @@
|
||||
# spring-modulith-events-api 2.1.1: the annotations that @ApplicationModuleListener carries (reflection)
|
||||
|
||||
@org.springframework.scheduling.annotation.Async("")
|
||||
@org.springframework.transaction.annotation.Transactional(propagation=REQUIRES_NEW, rollbackForClassName={}, readOnly=false, transactionManager="", isolation=DEFAULT, timeoutString="", label={}, noRollbackFor={}, noRollbackForClassName={}, value="", timeout=-1, rollbackFor={})
|
||||
@org.springframework.transaction.event.TransactionalEventListener(phase=AFTER_COMMIT, condition="", fallbackExecution=false, id="", value={}, classes={})
|
||||
@@ -0,0 +1,13 @@
|
||||
# DataSourceTransactionManager.doCleanupAfterCompletion in spring-jdbc 7.0.9: the calls it makes (javap -c)
|
||||
|
||||
org/springframework/jdbc/datasource/DataSourceTransactionManager$DataSourceTransactionObject.isNewConnectionHolder:()Z
|
||||
obtainDataSource:()Ljavax/sql/DataSource;
|
||||
org/springframework/transaction/support/TransactionSynchronizationManager.unbindResource:(Ljava/lang/Object;)Ljava/lang/Object;
|
||||
org/springframework/jdbc/datasource/DataSourceTransactionManager$DataSourceTransactionObject.getConnectionHolder:()Lorg/springframework/jdbc/datasource/ConnectionHolder;
|
||||
org/springframework/jdbc/datasource/ConnectionHolder.getConnection:()Ljava/sql/Connection;
|
||||
org/springframework/jdbc/datasource/DataSourceTransactionManager$DataSourceTransactionObject.isMustRestoreAutoCommit:()Z
|
||||
java/sql/Connection.setAutoCommit:(Z)V
|
||||
org/springframework/jdbc/datasource/DataSourceTransactionManager$DataSourceTransactionObject.getPreviousIsolationLevel:()Ljava/lang/Integer;
|
||||
org/springframework/jdbc/datasource/DataSourceTransactionManager$DataSourceTransactionObject.isReadOnly:()Z
|
||||
isDefaultReadOnly:()Z
|
||||
org/springframework/jdbc/datasource/DataSourceUtils.resetConnectionAfterTransaction:(Ljava/sql/Connection;Ljava/lang/Integer;Z)V
|
||||
Reference in New Issue
Block a user