My personal preference:
Open the transaction at the start of the request. Wire everything together for that request, injecting the transaction as required. Run the request. Close / roll back the transaction as required in the same method up at the top of the stack that you opened it.
Basically this makes it a request scoped bean (in Spring language), it’s just that instead of wiring the app together once at the start of the app, and then making the transaction available via a horrible proxy / thread local mechanism, you just wire the app together per request.