Mastering Spring @Scope: Custom Scopes, Prototypes, and Injection Techniques

This guide explains how Spring's @Scope annotation determines bean lifecycles, shows the underlying source code handling singleton and prototype scopes, demonstrates creating and registering a custom scope, and provides practical examples of using custom and prototype-scoped beans with proper proxy configuration in Spring Boot.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Mastering Spring @Scope: Custom Scopes, Prototypes, and Injection Techniques

Environment: Spring 5.3.3

1. Role of Scope

Using the @Scope annotation you can specify a bean's scope; by default beans are singletons (ConfigurableBeanFactory.SCOPE_SINGLETON = "singleton").

When a bean instance is created, Spring checks the BeanDefinition's scope to decide how to instantiate it. The relevant source code is shown below.

protected <T> T doGetBean(
            String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
            throws BeansException {
  String beanName = transformedBeanName(name);
  Object bean;
  // Eagerly check singleton cache for manually registered singletons.
  Object sharedInstance = getSingleton(beanName);
  if (sharedInstance != null && args == null) {
    // other code
  } else {
    // other code
    try {
      RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
      checkMergedBeanDefinition(mbd, beanName, args);
      // Guarantee initialization of beans that the current bean depends on.
      // other code
      // Create bean instance.
      // 根据BeanDefinition中定义的Scope创建实例
      // 判断如果是单例
      if (mbd.isSingleton()) {
        // 如果是单例Bean会将Bean保存到缓存中singletonObjects
        sharedInstance = getSingleton(beanName, () -> {
          try {
            return createBean(beanName, mbd, args);
          } catch (BeansException ex) {
            destroySingleton(beanName);
            throw ex;
          }
        });
        bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
      }
      // 判断如果是原型(多例)
      else if (mbd.isPrototype()) {
        // It's a prototype -> create a new instance.
        Object prototypeInstance = null;
        try {
          beforePrototypeCreation(beanName);
          prototypeInstance = createBean(beanName, mbd, args);
        } finally {
          afterPrototypeCreation(beanName);
        }
        bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
      } else {
        String scopeName = mbd.getScope();
        if (!StringUtils.hasLength(scopeName)) {
          throw new IllegalStateException("No scope name defined for bean " + beanName);
        }
        Scope scope = this.scopes.get(scopeName);
        // 当集合中也不存在时抛出异常
        if (scope == null) {
          throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
        }
        try {
          Object scopedInstance = scope.get(beanName, () -> {
            beforePrototypeCreation(beanName);
            try {
              return createBean(beanName, mbd, args);
            } finally {
              afterPrototypeCreation(beanName);
            }
          });
          bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
        } catch (IllegalStateException ex) {
          throw new BeanCreationException(beanName,
            "Scope '" + scopeName + "' 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",
            ex);
        }
      }
    } catch (BeansException ex) {
      cleanupAfterBeanCreationFailure(beanName);
      throw ex;
    }
  }
  // other code
  return (T) bean;
}

The code shows checks for singleton and prototype scopes; if neither matches, Spring looks up a custom Scope from the internal scopes map. If the custom scope is not registered, an exception is thrown; otherwise Spring invokes Scope#get to obtain the bean instance.

2. Defining a Custom Scope

Implement the Scope interface:

public class CustomScope implements Scope {

    private Object target;

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        return target != null ? target : objectFactory.getObject();
    }

    // If this method is called, subsequent injections of @Scope("custom") will invoke objectFactory.getObject().
    @Override
    public Object remove(String name) {
        target = null;
        return "success";
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
    }

    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    @Override
    public String getConversationId() {
        return null;
    }
}

Register the custom scope with the bean factory, typically via a BeanDefinitionRegistryPostProcessor:

@Component
public class CustomScopeRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        beanFactory.registerScope("custom", new CustomScope());
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
    }
}

Use the custom scope on a bean:

@Component
@Scope("custom")
public class ApplyScopeBean {
}

Example controller demonstrating both a prototype‑scoped bean and the custom scope:

@RestController
@RequestMapping("/refresh")
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class RefreshController implements ApplicationContextAware {

    @Resource
    private ApplyScopeBean scopeBean;

    @Resource
    private CustomScope customScope;

    @GetMapping("/custom")
    public String custom() {
        return scopeBean.getCustom();
    }

    @GetMapping("/remove")
    public Object remove() {
        return customScope.remove("applyScopeBean");
    }
}

Setting the controller itself as prototype allows observing the effect of creating new instances after each request; alternating calls to the endpoints will create fresh beans when the previous ones are removed.

3. Injecting Prototype Beans into Singletons

When a bean is declared with @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) and needs to be injected into a singleton, you must enable proxy mode so that each method call obtains a fresh instance:

@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ApplyScopeBean {
}

Without the proxy, the singleton will hold a single instance created at injection time. Alternatively, you can request a new instance directly from BeanFactory#getBean() each time.

End of tutorial.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

springdependency-injectionprototypescopeCustom Scope
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.