Skip to content

Make listener API less error prone #4808

Open
@micheljung

Description

@micheljung

Say you implement a skip listener like so in Kotlin:

class LoggingSkipListener<I, O>() : SkipListener<I, O> {
  override fun onSkipInRead(t: Throwable) {}
  override fun onSkipInWrite(item: O, t: Throwable) {}
  override fun onSkipInProcess(item: I, t: Throwable) {}
}

in your step, you accidentally use listener before faultTolerant:

    // WHOOPS - this calls listener(Object) rather than listener(SkipListener)
    .listener(LoggingSkipListener(inputKeyExtractor, outputKeyExtractor))
    .faultTolerant()

Which results in SimpleStepBuilder.listener(Object) being called rather than FaultTolerantStepBuilder.listener(SkipListener).

You realize your mistake and fix it:

    .faultTolerant()
    .listener(LoggingSkipListener(inputKeyExtractor, outputKeyExtractor))

Now it works. But damn, your I, O results in item being nullable. To fix this, you change them to I : Any, O : Any.
This will again result in listener(Object) being used (don't ask me why) but you don't notice it.

This is problematic because listener(Object) doesn't know the interface SkipListener and there are no annotated methods. Therefore, your listener is silently not registered anymore.

It seems that this would be easily fixable with something like

if (listener instanceof SkipListener) {
  skipListeners.add((SkipListener<I, O>) listener);
  return this;
}

I'd even go a step further and prevent listeners being passed that don't get registered:

if (listener instanceof SkipListener) {
  skipListeners.add((SkipListener<I, O>) listener);
  return this;
}

...

if (skipListenerMethods.isEmpty()) {
  throw new IllegalArgumentException("No skip listener methods found in: " + listener);
}

Activity

changed the title [-]Make listener API less fragile[/-] [+]Make listener API more fragile[/+] on Apr 8, 2025
changed the title [-]Make listener API more fragile[/-] [+]Make listener API less error prone[/+] on Apr 8, 2025
fmbenhassine

fmbenhassine commented on May 22, 2025

@fmbenhassine
Contributor

Thank you for reporting this issue! I prefer the fail fast approach: if the listener is not annotated as expected, it should be rejected with an exception rather than silently ignored.

Contributions are welcome!

added this to the 6.0.0 milestone on May 22, 2025
added a commit that references this issue on Jun 13, 2025
6927918
linked a pull request that will close this issue on Jun 13, 2025
SoxerL

SoxerL commented on Jun 21, 2025

@SoxerL

I tried to fix this in the implementation.
There is now a list of errors that is filled when there is a listener registration that does not use any of the annotations provided. When calling build() and there are any listener errors the method will throw a BuilderException.

There is one Tests I disabled, where I am not sure how it should be fixed. StepBuilderTests.testAnnotationBasedChunkListenerForJobStepBuilder as the test tries to create a chunkListener directly in a StepBuilderHelper, which as far as I could understand, does not check for any chunkListener methods and therefore this should not work by default.

I am happy to adapt my implementation if you have any guidance on how to solve this issue.

added 2 commits that reference this issue on Jun 21, 2025
53f620e
873b742
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

      Development

      Participants

      @fmbenhassine@micheljung@SoxerL

      Issue actions

        Make listener API less error prone · Issue #4808 · spring-projects/spring-batch