Fix duplicate listener registration in ChunkOrientedStepBuilder - #5478
Fix duplicate listener registration in ChunkOrientedStepBuilder#5478n-dlms wants to merge 1 commit into
Conversation
When an item handler implements a StepListener sub-interface and also has a method annotated with a listener annotation (e.g. @BeforeStep), the object was registered as a listener twice: once as the raw object and once as the proxy created by StepListenerFactoryBean. As a result, interface listener callbacks (e.g. ItemProcessListener.beforeProcess) were invoked twice per item. Make the two registration branches in addAsStreamAndListener mutually exclusive: use the factory branch when the object is or can be made into a StepListener, and only fall back to the direct instanceof branch when the factory does not consider the object a listener. Since the factory branch covers both interface implementations and annotation-based listeners, this restores the 5.2.x behavior of registering the object once. Fixes spring-projectsgh-5466 Signed-off-by: Ntokozo Dlamini <ntokozo.dlamini.xyz@gmail.com>
|
Hi @n-dlms, thanks for looking into this. Heads-up that this duplicates #5467, which I opened on July 22 (I reported #5466). The production change is functionally the same in both PRs, and the same as the "Suggested fix" in the issue description; only the comment wording and an inlined local differ. Yours is the better comment, since it says why the branches must be mutually exclusive. The real difference is the tests:
Both cover the regression, so whichever one the maintainers prefer is fine by me: I can close #5467, or fold your control case into it. Two nits here, if this one moves forward: |
|
Follow-up: I've added the control case to #5467 as well ( With that, the only remaining difference is the level the tests run at: #5467 starts a real job through |
|
@benelog Thanks for the follow-up, Sanghyuk. I appreciate you adding the control case to #5467 and for the clarification on the test-level difference. Glad the PR was useful in surfacing that case for the upstream tests. Either approach makes sense to me, so I'm happy to leave the final choice to the maintainers. |
Summary
Fixes #5466
When an
ItemReader,ItemProcessor, orItemWriterimplements aStepListenersub-interface (e.g.ItemProcessListener) and also has a method annotated with a listener annotation (e.g.@BeforeStep), the object is registered as a listener twice, so every interface callback fires twice per item. The annotated method itself fires once; only the interface callbacks are doubled.Root cause
ChunkOrientedStepBuilder#addAsStreamAndListenerhas two non-exclusive branches:When the object only implements the interface,
getListenerreturns the delegate itself, so theLinkedHashSetdeduplicates and callbacks fire once. But when an annotation is present,getListenerreturns a proxy (a different instance), so both the raw object and the proxy are registered and interface callbacks fire twice.This is a regression from 5.2.x where
SimpleStepBuilder#registerAsStreamsAndListenersused a singleStepListenerFactoryBean.isListener(...)branch.Fix
Make the two branches mutually exclusive, preferring the factory branch (it already covers both interface implementations and annotation-based listeners):
Tests
Added two regression tests to
ChunkOrientedStepTests:testListenerCallbacksFireOnceWhenProcessorImplementsListenerOnly— verifies interface callbacks fire exactly once per item (no annotations)testListenerCallbacksFireOnceWhenProcessorAlsoHasAnnotatedMethod— verifies interface callbacks fire exactly once per item even when a@BeforeStepannotated method is present (fails before the fix, passes after)Verification
spring-batch-coreunit suite: 806 tests run, 0 failures