Backend Development 7 min read

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.

<code>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;
}</code>

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:

<code>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;
    }
}
</code>

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

BeanDefinitionRegistryPostProcessor

:

<code>@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 {
    }
}
</code>

Use the custom scope on a bean:

<code>@Component
@Scope("custom")
public class ApplyScopeBean {
}
</code>

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

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

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:

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

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.

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

login 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.