Mastering Spring Bean Scopes: Custom Scope Creation and Prototype Injection

This article explains how Spring's @Scope annotation determines bean lifecycles, demonstrates creating and registering a custom scope, and shows how to correctly inject prototype-scoped beans into singleton components using scoped proxies.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Mastering Spring Bean Scopes: Custom Scope Creation and Prototype Injection

1. Bean Scope in Spring

In Spring 5.3.23, the @Scope annotation defines a bean's lifecycle. By default, beans are singletons ( ConfigurableBeanFactory.SCOPE_SINGLETON). When a bean is created, Spring checks the bean definition's scope and creates the instance accordingly.

protected <T> T doGetBean(String name, @Nullable Class<T> requiredType,
        @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {
    String beanName = transformedBeanName(name);
    Object bean;
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null && args == null) {
        // use existing singleton
    } else {
        try {
            RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
            checkMergedBeanDefinition(mbd, beanName, args);
            // guarantee initialization of dependent beans
            // create bean instance based on scope
            if (mbd.isSingleton()) {
                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()) {
                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;
        }
    }
    return (T) bean;
}

If the bean's scope is neither singleton nor prototype, Spring looks up the corresponding Scope implementation from the internal scopes map. If the custom scope is not registered, an exception is thrown.

2. Defining a Custom Scope

To create a custom scope, implement the org.springframework.beans.factory.config.Scope interface.

public class CustomScope implements Scope {
    private Object target;

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

    @Override
    public Object remove(String name) {
        target = null;
        return "success";
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        // no-op
    }

    @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 {
        // no additional processing needed
    }
}

Use the custom scope on a bean:

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

3. Prototype Injection into a Singleton

When a singleton bean depends on a prototype bean, you must enable a scoped proxy so that a new instance is obtained for each injection point.

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

Alternatively, you can request a new instance manually via BeanFactory#getBean() each time you need it.

4. Example Controller Using Custom and Prototype Scopes

@RestController
@RequestMapping("/refresh")
@Scope(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");
    }
}

In this controller, the bean ApplyScopeBean is defined with a custom scope, and the controller itself is prototype‑scoped to demonstrate the lifecycle behavior. Deleting the bean via the remove endpoint forces Spring to create a new instance on the next request.

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.

JavaspringprototypeBean ScopeCustom 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.