Spring单例模式下线程内共享数据 刚好这两天的开发任务有遇到多个Service共同依赖某些数据的情况,又不想通过修改接口参数去适配,就想了这个辙

在Spring Web开发中,一次请求内,逻辑涉及的多个模块均需要执行数据库查询获取结果,而事实上使用的是同一份数据,因此我们希望仅第一次执行真实查询、并将数据暂存起来,后续直接获取即可。

在Spring MVC中,Controller、Service、Dao默认都为单例模式,在大多数情况下,单例Bean是很理想的方案,降低了初始化和垃圾回收对象实例所带来的成本,但数据的环境隔离就不那么方便了。

Java中的类在实例化后其自身保存在JVM的堆区,成员变量作为类实例的一部分,自然也是保存在堆区,被所有线程共享。因此在单例模式下,保存在成员变量中的数据会被并发的多个线程同时访问,无法进行环境隔离,至于加锁的解决办法在Web应用高并发的情况下并不适用。

不过还好Spring在装配Bean时提供了作用域的选项,默认均为单例(singleton)模式。

Bean的作用域

Spring提供了四种作用域的选项,在@Scope注解的源码中可见:

  1. /**
  2. * Specifies the name of the scope to use for the annotated component/bean.
  3. * <p>Defaults to an empty string ({@code ""}) which implies
  4. * {@link ConfigurableBeanFactory#SCOPE_SINGLETON SCOPE_SINGLETON}.
  5. * @since 4.2
  6. * @see ConfigurableBeanFactory#SCOPE_PROTOTYPE
  7. * @see ConfigurableBeanFactory#SCOPE_SINGLETON
  8. * @see org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
  9. * @see org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
  10. * @see #value
  11. */

作用域名称 描述
单例(Singleton) 在整个应用中,只会创建一个bean的实例
原型(Prototype) 每次注入或者通过上下文获取的时候,都会创建一个新的bean实例
会话(Session) 在Web应用中,为每个会话创建一个bean实例
请求(Request) 在Web应用中,为每个请求创建一个bean实例

因此我们可以利用Spring提供的Request作用域来实现在单次请求内的数据缓存。

eg:

  1. @Component
  2. @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
  3. public class RequestScopeCache {
  4. private Map<UUID, List<PResourceRelation>> local1 = new HashMap<>();
  5. /**
  6. * Sets map to resource relation list.
  7. *
  8. * @param mapId the map id
  9. * @param resourceRelations the resource relations
  10. */
  11. public void setMapToResourceRelationList(UUID mapId, List<PResourceRelation> resourceRelations) {
  12. local1.put(mapId, resourceRelations);
  13. }
  14. /**
  15. * Gets map to resource relation list.
  16. *
  17. * @param mapId the map id
  18. * @return the map to resource relation list
  19. */
  20. public List<PResourceRelation> getMapToResourceRelationList(UUID mapId) {
  21. return local1.get(mapId);
  22. }
  23. }

在请求所经历的Controller、Service中,通过@Autowired注入即可,可以实现单次请求内的数据共享。

额外需要注意的是@Scope注解中的proxyMode属性,当在单例模式的Bean中注入非单例模式的Bean时,该属性会让Spring在单例Bean实例化时先不要注入非单例Bean的实例,而是根据上下文环境选择何时实例化Bean。查看该属性注释可见:

  1. /**
  2. * Specifies whether a component should be configured as a scoped proxy
  3. * and if so, whether the proxy should be interface-based or subclass-based.
  4. * <p>Defaults to {@link ScopedProxyMode#DEFAULT}, which typically indicates
  5. * that no scoped proxy should be created unless a different default
  6. * has been configured at the component-scan instruction level.
  7. * <p>Analogous to {@code <aop:scoped-proxy/>} support in Spring XML.
  8. * @see ScopedProxyMode
  9. */

当代理的为具体Class时,使用ScopedProxyMode.TARGET_CLASS;当代理的为接口(Interface)时,则使用ScopedProxyMode.INTERFACES;默认为DEFAULT。