Lesson

Custom Scope

spring/Spring Boot

Custom Bean Scope in Spring

1. Introduction

In Spring, bean scope defines:

  • Lifecycle of a bean

  • Visibility of a bean

Spring provides built-in scopes:

  • Singleton

  • Prototype

  • Request

  • Session

  • Application

But sometimes these are not enough. In such cases, we create custom bean scopes.


2. Why Custom Bean Scope

Custom scopes are useful when:

  • You need custom lifecycle control

  • Built-in scopes do not fit your requirement

  • You want to manage beans based on:

    • Thread

    • User

    • Tenant


3. When to Use Custom Scope

Use custom scope when:

  • Bean should live longer than request but not singleton

  • You want per-thread bean instance

  • You want custom lifecycle behavior


4. Steps to Create Custom Scope


Step 1: Add Dependency

plaintext
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.1.6</version>
</dependency>

Step 2: Implement Scope Interface

Spring provides:

plaintext
org.springframework.beans.factory.config.Scope

Create custom class:

plaintext
package com.example.scope;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ThreadLocalScope implements Scope {

    private final ThreadLocal<Map<String, Object>> threadLocal =
            ThreadLocal.withInitial(ConcurrentHashMap::new);

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {

        Map<String, Object> scopedObjects = threadLocal.get();

        return scopedObjects.computeIfAbsent(name,
                k -> objectFactory.getObject());
    }

    @Override
    public Object remove(String name) {
        return threadLocal.get().remove(name);
    }

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

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

    @Override
    public String getConversationId() {
        return Thread.currentThread().getName();
    }
}

Explanation


  • Uses ThreadLocal to store bean instances


  • Each thread gets its own bean


  • Beans are not shared across threads


Step 3: Register Custom Scope

plaintext
package com.example.config;

import com.example.scope.ThreadLocalScope;
import com.example.service.MyBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.config.CustomScopeConfigurer;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class AppConfig {

    @Bean
    public static CustomScopeConfigurer customScopeConfigurer() {

        CustomScopeConfigurer configurer =
                new CustomScopeConfigurer();

        Map<String, Object> scopes = new HashMap<>();

        scopes.put("thread-local", new ThreadLocalScope());

        configurer.setScopes(scopes);

        return configurer;
    }

    @Bean
    @org.springframework.context.annotation.Scope("thread-local")
    public MyBean myBean() {
        return new MyBean();
    }
}

Step 4: Create Bean Class

plaintext
package com.example.service;

public class MyBean {

    public MyBean() {
        System.out.println("MyBean instance created");
    }
}

Step 5: Test Custom Scope

plaintext
package com.example;

import com.example.config.AppConfig;
import com.example.service.MyBean;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class CustomScopeDemo {

    public static void main(String[] args) {

        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(AppConfig.class);

        Runnable task = () -> {

            MyBean bean1 = context.getBean(MyBean.class);
            MyBean bean2 = context.getBean(MyBean.class);

            System.out.println(Thread.currentThread().getName()
                    + " : " + (bean1 == bean2));
        };

        Thread t1 = new Thread(task, "Thread-1");
        Thread t2 = new Thread(task, "Thread-2");

        t1.start();
        t2.start();
    }
}

5. Output Understanding

  • Same thread → same bean instance → true

  • Different thread → different bean instance → false


6. Advantages of Custom Scope

  • Full control over bean lifecycle

  • Supports thread-based or user-based logic

  • Efficient resource usage

  • Better modular design


7. Conclusion

  • Custom bean scope is used in advanced scenarios

  • ThreadLocal scope is a common example

  • Helps manage beans beyond default Spring scopes