Annotation Interface AuditedOnError


@Retention(RUNTIME) @Target(METHOD) @Repeatable(AuditedOnErrors.class) @Documented public @interface AuditedOnError
Throwing cousin of 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:

  • @AfterThrowing only — methods that return normally record nothing through this annotation. Pair with Audited or AuditedConditional on the same method to also record success.
  • The thrown Throwable is 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 in AUDIT_EVENT.DETAIL.
  • The evaluation context for messageSpel() is identical to Audited.messageSpel() EXCEPT that #result is 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 to message() (a literal). If both are empty the audit row is still written with #exception.message as the note (the default for messageSpel()), so a thrown event never loses its note silently. Contrast with AuditedConditional, whose when predicate 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 Elements
    Modifier and Type
    Required Element
    Description
    The concrete AuditEventType subclass to record.
  • Optional Element Summary

    Optional Elements
    Modifier and Type
    Optional Element
    Description
    Class<? extends Throwable>
    Optional exception-class filter.
    Optional plain (non-SpEL) note string.
    Optional SpEL expression for the note, evaluated against the post-throw context.
  • Element Details

    • value

      Class<? extends AuditEventType> value
      The concrete AuditEventType subclass to record. Must be instantiable via a public no-arg constructor. Conventionally a Failed…Event subtype.
    • exception

      Class<? extends Throwable> exception
      Optional exception-class filter. The annotation fires only when the caught throwable is an instanceof this class. Default Throwable matches everything (backwards-compatible with the pre-repeatable shape — a single un-filtered @AuditedOnError behaves 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 exception class is the deepest in the instanceof chain. A default Throwable.class declaration 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 message
      Optional plain (non-SpEL) note string. Stored verbatim in AUDIT_EVENT.NOTE when messageSpel() is empty or fails to evaluate. Empty default means "fall through to the SpEL default".
      Default:
      ""
    • messageSpel

      String messageSpel
      Optional SpEL expression for the note, evaluated against the post-throw context. The caught Throwable is 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"