Spring, the versatile framework, empowers developers with an array of remarkable features for managing the lifecycle of beans. Typically, Spring handles the bean lifecycle when we utilize annotations like @Service or @Component. As the Spring Container starts up, these beans are created, and upon its shutdown, they are gracefully destroyed. However, beyond these familiar methods, there exists a wealth of possibilities to fine-tune the lifecycle of beans.
In this blog post, we'll dive into an aspect of Spring known as "Spring Bean Scopes." Specifically, we'll explore the versatile capabilities of a request scoped bean and discover how it can be utilized beyond the boundaries of a web-based request.
RequestScope operates seamlessly within a web-based context when handled by the Spring DispatcherServlet. However, it poses an issue when attempting to access a request scoped bean outside the bounds of a web request. You may encounter an exception similar to the following:
java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
Nevertheless, this limitation is not a flaw in Spring's design. The framework does not expose the RequestScope since it cannot determine when a request starts and ends outside of the DispatcherServlet. Instead, it expects developers to take charge of this responsibility, and we can achieve this through the following steps:
Create a CustomRequestScopeAttr (Mostly copied from blog post of Pranav Maniar)
This will only be available and working for request scoped beans and not for other scopes as well.
Next we need to manually set the RequestAttributes. This needs to be done where logically our request scoped process starts. Its done by calling:
Once the request scoped process is finished, we reset the RequestAttributes by calling:
By calling these methods RequestContextHolder.setRequestAttributes(new CustomRequestScopeAttr()) to start the process and RequestContextHolder.resetRequestAttributes() to end it - we take control of determining when a request begins and concludes.
In the following sections, we will explore various scenarios to illustrate how we can determine the start and end of a request effectively.
Encountering an @Async annotation within a web-based request can lead to an IllegalStateException when attempting to access a request scoped bean. To overcome this, we need to set the RequestAttributes when creating the async thread. This involves creating a custom AsyncConfiguration to manage async thread creation, where we set the RequestAttributes accordingly.
Following is an example of an AsyncConfiguration:
When you enter an @Async block, getAsyncExecutor() will be called, and "taskExecutor" is the default name. Alternatively, you can pass an executor name inside the @Async annotation. The configuration is then passed to our custom ContextAwarePoolExecutor, which is defined as:
The ContextAwarePoolExecutor handles the actual execution of the newly created thread and ensures that the RequestAttributes are properly set:
The ContextAwareCallable class implements the Callable interface and handles the execution of the new thread. In its call() method, we set the RequestAttributes to "mark" the start of the request. As we pass the RequestAttributes from the main thread, we need to clone them before setting. This prevents issues where the main thread might finish before the async thread, leading to attribute garbage collection. In the finally-block of the call() method, we "mark" the end of the request by resetting the RequestAttributes. This approach allows for cascading multiple async calls, because request attributes being passed down and always cloned for each new thread.
By using this mechanism, you can seamlessly utilize @Async annotations while retaining the functionality of request scoped beans.
The firewall rule for the connection to the backend only applies to a specific tag, which looks like a node. For example: "gke-staging-456b4340-node "However this is a Network Tag, which is on every Compute Instance of the cluster. Therefor the heaWhen working with Pub/Sub consume events, which are requests from a queue rather than web-based interactions, using request scoped beans requires the setting of RequestAttributes. Fortunately, defining the start and end of a request in a Pub/Sub "context" is straightforward, as we have clear visibility into when a call begins and concludes. In our case, we use Spring Cloud GCP Pub/Sub.
We have a PubSubConsumer class which somehow looks like this:
When starting the consume process in the startConsumeProcess() method, we set the RequestScope attributes. In the finally-block of the actual Receiver, we reset the RequestAttributes. The entire Pub/Sub event, from the beginning of the consumer until the end, forms a complete request, ensuring that the management of request scoped beans works seamlessly within this context.
Even if you invoke an asynchronous method within the consumer, this approach remains effective. Just make sure to implement the changes from the "Async" chapter to handle asynchronous scenarios correctly. By combining these strategies, you can confidently leverage request scoped beans in Pub/Sub events, facilitating robust and efficient processing of messages within your Spring application.
By now I sadly have not found a feasible / generic solution to access a request scoped bean inside of a java ParallelStream. The underlying issue lies in the use of a common fork/join pool by Java streams for parallelization. These threads are neither created nor configured by Spring or the developer, unlike what we did in the "Async" section. While you could manually set the request attributes for each ParallelStream, this is not a practical solution, especially when already dealing with multiple ParallelStreams, leading to potential oversights.
Another approach would be to create a custom ForkJoinPool and use this for parallelstreaming. This may look something like this (copied from here):
This approach requires explicitly submitting tasks to the custom forkJoinPool each time you wish to perform parallel operations, making the use of ParallelStreams impractical.
We have made the decision to accept the limitation of not being able to use request-scoped beans inside ParallelStreams. Instead, we adopt alternative strategies to achieve our goals without relying on request-scoped beans in these parallel execution scenarios. While this limitation may present certain constraints, understanding and working around it allow us to use ParallelStreams effectively within our Spring application.
Request scoped beans in Spring offer significant flexibility, extending their usage beyond traditional web-based requests. However, when using these beans outside of the standard web context, developers must take responsibility for defining the start and end of each process. Spring cannot automatically manage this in custom environments or process flows. Throughout various scenarios, such as asynchronous processing, pub/sub events, and custom thread pools, we've demonstrated effective techniques for handling request scoped beans. The only limitation we encountered thus far is with ParallelStreams, where a generic solution for accessing request-scoped beans is still missing for us. Despite this limitation, understanding the constraints and employing alternative strategies empower developers to leverage the full potential of request-scoped beans within their Spring applications.
Kai Müller
Software Engineer