Annotation Interface AuditedOnError
Audited / AuditedConditional. Method-level
annotation that asks AuditedAspect to emit an audit event of the
declared AuditEventType when the method exits by throwing.
Motivated by AUDIT_PHASE_C_RECCE.md bucket 2e: a cluster of
imperative auditTrailService.addUpdateEvent(target, FailedXEvent.class,
e.getMessage(), e) callers sit inside catch blocks and re-throw the
exception after writing the audit row. The canonical shape is:
try {
doWork( ee );
} catch ( Exception e ) {
auditTrailService.addUpdateEvent( ee, FailedFooEvent.class, e.getMessage(), e );
throw e;
}
With this annotation, the body collapses to a plain doWork(ee) call
on a method annotated @AuditedOnError(FailedFooEvent.class).
Semantics:
@AfterThrowingonly — methods that return normally record nothing through this annotation. Pair withAuditedorAuditedConditionalon the same method to also record success.- The thrown
Throwableis re-thrown verbatim after the audit row is written — the aspect does NOT swallow exceptions. - The audit row is written through
AuditTrailService.addUpdateEvent(Auditable, Class, String, Throwable), which carries@Transactional(propagation = REQUIRES_NEW)so the row survives the rollback of the surrounding transaction. The full stack trace is persisted inAUDIT_EVENT.DETAIL. - The evaluation context for
messageSpel()is identical toAudited.messageSpel()EXCEPT that#resultis undefined (the method threw, there is no return value) and the caught throwable is exposed as#exception. Method parameters resolve by name (requires-parameters, enabled project-wide). - If the
messageSpel()SpEL fails to evaluate, the aspect falls back tomessage()(a literal). If both are empty the audit row is still written with#exception.messageas the note (the default formessageSpel()), so a thrown event never loses its note silently. Contrast withAuditedConditional, whosewhenpredicate failure SKIPS emission.
A method may carry Audited (or AuditedConditional) AND
@AuditedOnError simultaneously — the success annotation fires on
normal return, this one fires on throw, and they share no advice path.
Repeatable with exception-class dispatch. The annotation is
Repeatable via AuditedOnErrors, and exposes an
exception() filter so that a single method can record DIFFERENT
Failed…Event types for different caught exception classes — the
canonical multi-catch shape:
@AuditedOnError(value = BatchInformationMissingEvent.class,
exception = BatchInfoMissingException.class)
@AuditedOnError(value = FailedBatchInformationFetchingEvent.class) // default Throwable.class → fallback
public void fillBatchInformation( ExpressionExperiment ee ) { ... }
Dispatch rule: at most ONE matching declaration emits per throw.
When several declarations would match (e.g. a Throwable.class
default-filter plus a specific subclass filter), the aspect picks the
MOST-SPECIFIC match — the declaration whose exception() class is
lowest in the instanceof chain for the thrown throwable. Ties are
resolved by declaration order (first wins) but ties are not expected in
normal use. A default Throwable.class declaration therefore acts as
the "catch-all" fallback in mirror to a Java catch (Exception e)
block at the bottom of a multi-catch.
-
Required Element Summary
Required ElementsModifier and TypeRequired ElementDescriptionClass<? extends AuditEventType> The concreteAuditEventTypesubclass to record. -
Optional Element Summary
Optional Elements
-
Element Details
-
value
Class<? extends AuditEventType> valueThe concreteAuditEventTypesubclass to record. Must be instantiable via a public no-arg constructor. Conventionally aFailed…Eventsubtype. -
exception
Optional exception-class filter. The annotation fires only when the caught throwable is aninstanceofthis class. DefaultThrowablematches everything (backwards-compatible with the pre-repeatable shape — a single un-filtered@AuditedOnErrorbehaves identically before and after this change).When multiple repeated declarations could match a given throwable, the aspect picks the MOST-SPECIFIC declaration — the one whose
exceptionclass is the deepest in theinstanceofchain. A defaultThrowable.classdeclaration on the same method therefore acts as a catch-all fallback that fires only when no more-specific declaration matches.- Default:
java.lang.Throwable.class
-
message
String messageOptional plain (non-SpEL) note string. Stored verbatim inAUDIT_EVENT.NOTEwhenmessageSpel()is empty or fails to evaluate. Empty default means "fall through to the SpEL default".- Default:
""
-
messageSpel
String messageSpelOptional SpEL expression for the note, evaluated against the post-throw context. The caughtThrowableis exposed as#exception; method parameters resolve by name; the root object is the args array (so#root.args[i]also works). The default"#exception.message"matches the most common imperative pattern:addUpdateEvent(ee, type, e.getMessage(), e).If evaluation throws, the aspect falls back to
message(); the audit row is still written so a thrown exception is never silently lost from the audit log.- Default:
"#exception.message"
-