You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让Spring Bean同时具备Request和Schedule两种作用域?

问题描述

我有一个用于请求期间缓存的UsersHandledInRequestCache Bean,配置如下:

@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);
        }
    }
}

对应的UserService会注入这个缓存Bean,用来避免重复处理同一个用户ID:

@Service
public class UserService {

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

    public void doSomething(Integer userId) {
        if (!usersHandledInRequestCache.isCached(userId)) {
            // 执行业务逻辑
            usersHandledInRequestCache.cache(userId);
        }
    }
}

当Web Controller调用这个服务时完全正常,每个HTTP请求都会创建独立的缓存实例,不会出现重复处理的问题:

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

    private final UserService userService;

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

但我还有一个定时任务,同样会调用UserService的方法:

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

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

定时任务运行时直接抛出异常,原因很明显——定时任务没有HTTP请求上下文,@RequestScope的Bean无法初始化:

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.

我需要让这个缓存Bean同时支持请求上下文和定时任务上下文,该怎么实现?


解决方案

方法一:自定义混合作用域(推荐)

自己实现一个Scope,让它在有请求上下文时复用request作用域的逻辑,没有时用线程局部存储(对应定时任务的单次执行线程)。

1. 实现自定义Scope类

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.context.support.SimpleThreadScope;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

public class RequestOrScheduleScope extends SimpleThreadScope {

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        // 先检查是否存在HTTP请求上下文
        RequestAttributes requestAttrs = RequestContextHolder.getRequestAttributes();
        if (requestAttrs != null) {
            // 请求上下文下,直接用request作用域存储Bean
            Object bean = requestAttrs.getAttribute(name, RequestAttributes.SCOPE_REQUEST);
            if (bean == null) {
                bean = objectFactory.getObject();
                requestAttrs.setAttribute(name, bean, RequestAttributes.SCOPE_REQUEST);
            }
            return bean;
        } else {
            // 无请求上下文(比如定时任务),用线程局部存储
            return super.get(name, objectFactory);
        }
    }

    @Override
    public void remove(String name) {
        RequestAttributes requestAttrs = RequestContextHolder.getRequestAttributes();
        if (requestAttrs != null) {
            requestAttrs.removeAttribute(name, RequestAttributes.SCOPE_REQUEST);
        } else {
            super.remove(name);
        }
    }
}

2. 注册自定义作用域并修改Bean配置

import org.springframework.beans.factory.config.CustomScopeConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;

@Configuration
public class CacheConfiguration {

    // 注册自定义的混合作用域
    @Bean
    public static CustomScopeConfigurer customScopeConfigurer() {
        CustomScopeConfigurer configurer = new CustomScopeConfigurer();
        configurer.addScope("requestOrSchedule", new RequestOrScheduleScope());
        return configurer;
    }

    // 把Bean的作用域改成自定义的requestOrSchedule,同时开启代理
    @Bean
    @Scope(value = "requestOrSchedule", proxyMode = ScopedProxyMode.TARGET_CLASS)
    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);
        }

        // 新增清理方法,用于定时任务线程复用后清理缓存
        public void clear() {
            this.cachedUsersIds.clear();
        }
    }
}

3. 定时任务中手动清理缓存

因为定时任务线程通常会被线程池复用,所以在任务执行完成后必须清理缓存,避免下一次任务拿到残留数据:

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

    @Scheduled(cron = "0 0 7 * * *")
    public void doSomething() {
        try {
            userService.getSomeUsersIds.forEach(userService::doSomething);
        } finally {
            // 不管任务成功失败,都清理缓存
            usersHandledInRequestCache.clear();
        }
    }
}

方法二:直接用ThreadLocal实现(简单粗暴)

如果不想自定义Scope,可以直接用ThreadLocal来存储缓存,天然支持线程隔离,不管是Web请求线程还是定时任务线程都能适配。

修改缓存Bean的实现

@Configuration
public class CacheConfiguration {

    @Bean
    // 这里用单例即可,因为缓存存在ThreadLocal里
    public UsersHandledInRequestCache usersHandledInRequestCache() {
        return new UsersHandledInRequestCache();
    }

    public static class UsersHandledInRequestCache {
        // 用ThreadLocal维护每个线程独立的缓存集合
        private final ThreadLocal<Set<Integer>> cachedUsersIds = ThreadLocal.withInitial(HashSet::new);

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

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

        // 清理当前线程的缓存
        public void clear() {
            cachedUsersIds.remove();
        }
    }
}

补充清理逻辑

  • Web请求场景:需要在请求结束后清理ThreadLocal,避免Tomcat线程池复用导致缓存残留。可以用拦截器实现:
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class CacheCleanInterceptor implements HandlerInterceptor {

    private final UsersHandledInRequestCache cache;

    public CacheCleanInterceptor(UsersHandledInRequestCache cache) {
        this.cache = cache;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 请求完成后清理缓存
        cache.clear();
    }
}

注册拦截器:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final CacheCleanInterceptor cacheCleanInterceptor;

    public WebConfig(CacheCleanInterceptor cacheCleanInterceptor) {
        this.cacheCleanInterceptor = cacheCleanInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(cacheCleanInterceptor);
    }
}
  • 定时任务场景:同样在finally块中清理缓存,和方法一的定时任务代码一致。

方法三:手动模拟请求上下文(不推荐)

这种方式属于hack,强行在定时任务中创建一个空的请求上下文,让@RequestScope的Bean能初始化:

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.MockHttpServletRequest;

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

    @Scheduled(cron = "0 0 7 * * *")
    public void doSomething() {
        // 手动创建一个空的请求上下文
        RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(new MockHttpServletRequest()));
        try {
            userService.getSomeUsersIds.forEach(userService::doSomething);
        } finally {
            // 用完必须销毁上下文
            RequestContextHolder.resetRequestAttributes();
        }
    }
}

这种方法虽然能解决问题,但不符合语义——定时任务本来就和HTTP请求无关,不建议在生产环境使用。


内容的提问来源于stack exchange,提问作者Zhenyria

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 14:07:59