Versions: Spring Boot 2.2.2, Spring Cloud Hoxton.RELEASE Runnable demo: https://drive.google.com/open?id=1ThjcCoJToHKAhWYhpOIyGzi9xIoYtxRY

In my application, I have the following @Configuration to add support for @RequestScope bean within @Async methods.

@Configuration
@EnableAsync
public class AsyncExecutionConfiguration extends AsyncConfigurerSupport {`
    public static final String ASYNC_THREAD_NAME_PREFIX = "async-worker-";

    @Override
    @Primary
    @Bean("taskExecutorWithClonedRequestContext")
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // Copy the current RequestContext to each and every new @Async task
        executor.setTaskDecorator(new RequestContextCloningDecorator());
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(20);
        executor.setThreadNamePrefix(ASYNC_THREAD_NAME_PREFIX);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();

        return executor;
    }
}

I'm using the following RequestContextCloningDecorator to clone the request context on every @Async call.

public class RequestContextCloningDecorator implements TaskDecorator {
    @Nonnull
    @Override
    public Runnable decorate(@Nonnull Runnable runnable) {
        RequestAttributes requestContext = this.cloneRequestAttributes(RequestContextHolder.currentRequestAttributes());

        return () -> {
            try {
                RequestContextHolder.setRequestAttributes(requestContext);
                runnable.run();

            } finally {
                RequestContextHolder.resetRequestAttributes();

            }
        };
    }

    private RequestAttributes cloneRequestAttributes(RequestAttributes requestAttributes) {
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;

        try {
            RequestAttributes clonedRequestAttribute = new ServletRequestAttributes(servletRequestAttributes.getRequest(), servletRequestAttributes.getResponse());

            // Since we're not using session scope, we only need to copy request scope attributes
            String[] scopeAttributes = requestAttributes.getAttributeNames(RequestAttributes.SCOPE_REQUEST);
            if (scopeAttributes.length > 0) {
                for (String attrName : scopeAttributes)
                    clonedRequestAttribute.setAttribute(attrName,
                                                        requestAttributes.getAttribute(attrName, RequestAttributes.SCOPE_REQUEST),
                                                        RequestAttributes.SCOPE_REQUEST);
            }

            return clonedRequestAttribute;

        } catch (Exception e) {
            return requestAttributes;

        }
    }
}

Everything has worked fine so far until I have a new feature which involves an @Async method with a loop inside of it. I can reproduce the bug consistently with 3 simple beans.

First, this is the RequestScopeBean which does nothing but holding an int value, default to 99.

@RequestScope
@Component
public class RequestScopeBean {
    private int value = 99;

    public void updateValue(int newValue) {
        value = newValue;
    }

    public int getCurrentValue() {
        return value;
    }
}

This is the @Service which contains an @Async method with a loop inside.

@Service
public class AsyncService {
    @Autowired
    private RequestScopeBean mrBean;

    @Async
    public void loopAsync() {
        for (int i = 0 ; i < 1000 ; i++) {
            if  (mrBean.getCurrentValue() != 1234) {
                System.out.println("=================");
                System.out.println("SERVICE BEAN: " + mrBean);
                System.out.println("INDEX: " + i + " - CUR VALUE: " + mrBean.getCurrentValue());
            }
        }
    }
}

Finally, this is our entry point.

@RestController
@RequestMapping("/async")
public class AsyncController {
    @Autowired
    private AsyncService asyncService;

    @Autowired
    private RequestScopeBean mrBean;

    @GetMapping("/test")
    public void test() {
        mrBean.updateValue(1234);

        System.out.println("=================");
        System.out.println("CONTROLLER BEAN: " + mrBean);
        System.out.println("CUR VALUE: " + mrBean.getCurrentValue());

        asyncService.loopAsync();
    }
}

When I call this endpoint, I'll see something like this.

=================
CONTROLLER BEAN: com.ft.demo.helper.RequestScopeBean@2278b137
CUR VALUE: 1234
=================
SERVICE BEAN: com.ft.demo.helper.RequestScopeBean@1f1b5a8c
INDEX: 865 - CUR VALUE: 99
=================
SERVICE BEAN: com.ft.demo.helper.RequestScopeBean@1f1b5a8c
INDEX: 866 - CUR VALUE: 99
=================
SERVICE BEAN: com.ft.demo.helper.RequestScopeBean@1f1b5a8c
INDEX: 867 - CUR VALUE: 99
......
=================
SERVICE BEAN: com.ft.demo.helper.RequestScopeBean@1f1b5a8c
INDEX: 999 - CUR VALUE: 99

Within the loop, at any random point of time, the @RequestScope bean will suddenly be swapped with a brand new instance, which carries the default value instead of the updated value.

When I wrote my code, I mainly referred to the solutions suggested in this StackOverflow question. If there's a better way to do this, please let me know. Otherwise, I think we should have a fix for this buggy behaviour? :)

Comment From: rstoyanchev

The cloning creates a fresh instance of ServletRequestAttributes but this class is a simple facade around the HttpServletRequest and HttpServletResponse, so the clone is not deep. When the controller method returns and request handling is over, the Servlet container re-uses the request and response for the next request.

In order for this to work, either request handling has to be asynchronous (e.g. returning CompletableFuture) and complete only after async handling is done. Or the copy has to be made deep through a different implementation of RequestAttributes.

Comment From: matzegebbe

Could someone update the example to use a deep copy of the request attributes in the asynchronous task or get called from outside with direct 200 response and trigger the async task which can use the request attributes multiple times.

many thanks in advance!