javaspringscopescheduled-tasks

How to create bean with "request" and "schedule" scopes at one time?


I have UsersHandledInRequestCache bean which is using for caching during request:

@Configuration
public class CacheConfiguration {

    @Bean
    @RequestScope
    public UsersHandledInRequestCache usersHandledInRequestCache() {
        return new UsersHandledInRequestCache();
    }

    public static class UsersHandledInRequestCache {
        private final Set<Integer> cachedUsersIds = new HashSet<>();

        public void cache(Integer userId) {
            this.cachedUsersIds.add(userId);
        }

        public boolean isCached(Integer userId) {
            return this.cachedUsersIds.contains(userId);
        }
    }
}

And i have a service which uses the bean:

@Service
public class UserService {

    @Resource(name = "usersHandledInRequestCache")
    private CacheConfiguration.UsersHandledInRequestCache usersHandledInRequestCache;

    public void doSomething(Integer userId) {
        if (!usersHandledInRequestCache.isCached(userId)) {
            // do something
            usersHandledInRequestCache.cache(userId);
        }
    }
}

So the service can be called by controllers and there are no problems:

@RestController
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserController {

    private final UserService userService;

    @PostMapping
    public void doSomething(@RequestBody Collection<Integer> usersIds) {
        usersIds.forEach(userService::doSomething);
    }
}

In this case UsersHandledInRequestCache bean is creating for each request and works correct. But additionaly i have scheduled service:

@Component
@RequiredArgsConstructor
public class UserJob {
    
    private final UserService userService;

    @Scheduled(cron = "0 0 7 * * *")
    public void doSomething() {
        userService.getSomeUsersIds.forEach(userService::doSomething);
    }
}

And when the scheduled service is starting to work i get exception due to field UserService#usersHandledInRequestCache can not be initialized (because there are no any requests and method calling is initiated by scheduled service work):

Error creating bean with name 'scopedTarget.usersHandledInRequestCache': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is 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: In this case, use RequestContextListener or RequestContextFilter to expose the current request.

I want to make bean UsersHandledInRequestCache with "request" scope and "schedule" scope at one time. How i can get it?


Solution

  • The above error occurs because the spring request scope is only registered for web app requests.

    Through the implementation of the RequestAttribute interface, spring puts/gets the request scope information from HttpRequest.

    If the thread was started outside of the web request, the thread does not have the required attributes in its variables and hence throws exception.

    I ran into the same problem - needed to execute code in the job using @Scheduled, so it was unable to use any Session or Request scope beans.

    There are couple of ways to solve this problem, I solved it the following way:

    Implement RequestAttributes Interface:
    import org.springframework.web.context.request.RequestAttributes;
    import java.util.HashMap;
    import java.util.Map;
    
    public class CustomRequestAttribute implements RequestAttributes {
    
        private Map<String, Object> requestAttributes = new HashMap<>();
    
        @Override
        public Object getAttribute(String name, int scope) {
            if (scope == RequestAttributes.SCOPE_REQUEST) {
                return this.requestAttributes.get(name);
            } else return null;
        }
    
        @Override
        public void setAttribute(String name, Object value, int scope) {
            if (scope == RequestAttributes.SCOPE_REQUEST) {
                this.requestAttributes.put(name, value);
            }
        }
    
        @Override
        public void removeAttribute(String name, int scope) {
            if (scope == RequestAttributes.SCOPE_REQUEST) {
                this.requestAttributes.remove(name);
            }
        }
    
        @Override
        public String[] getAttributeNames(int scope) {
            if (scope == RequestAttributes.SCOPE_REQUEST) {
                return this.requestAttributes.keySet().toArray(new String[0]);
            } else return new String[0];
        }
    
        @Override
        public void registerDestructionCallback(String name, Runnable callback, int scope) {}
    
        @Override
        public Object resolveReference(String key) { return null; }
    
        @Override
        public String getSessionId() { return null; }
    
        @Override
        public Object getSessionMutex() { return null; }
    }
    

    Now at the start of the request in your code, instruct the RequestContextHolder to use this CustomRequestAttribute by executing the following statement and in the finally block clear out the request attributes.

    @Scheduled(cron = "0 0 7 * * *")
    public void doSomething() {
        try {
            RequestContextHolder.setRequestAttributes(
                    new CustomRequestAttribute());
            userService.getSomeUsersIds
                    .forEach(userService::doSomething);
        } catch (Exception e) {
            // ...
        } finally {
            RequestContextHolder.resetRequestAttributes();
        }
    }