← Назад к вопросам

Можно ли создать свой Scope бинов?

3.0 Senior🔥 81 комментариев
#Spring Framework

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Создание собственного Scope для бинов в Spring

Отличный вопрос о Spring Container и управлении жизненным циклом бинов! Короткий ответ: ДА, полностью можно — Spring предоставляет механизм для создания custom scopes через интерфейс Scope. Это мощный инструмент для специальных сценариев.

Встроенные Scopes в Spring

Сначала напомню встроенные scopes:

  • Singleton — один экземпляр на весь контейнер (по умолчанию)
  • Prototype — новый экземпляр при каждом запросе
  • Request — один экземпляр на HTTP запрос (web приложения)
  • Session — один экземпляр на HTTP сессию
  • Application — один экземпляр на ServletContext
  • WebSocket — один экземпляр на WebSocket сессию
@Component
@Scope("singleton")  // По умолчанию
public class UserService {}

@Component
@Scope("prototype")  // Новый при каждом запросе
public class RequestHandler {}

@Component
@Scope("request")  // Один на HTTP request
public class HttpRequestData {}

Создание собственного Scope

Шаг 1: Реализуем интерфейс Scope

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

public class TenantScope implements Scope {
    private final Map<String, Object> beans = new ConcurrentHashMap<>();
    
    // Возвращает объект из scope по имени
    public Object get(String name, ObjectFactory<?> objectFactory) {
        return beans.computeIfAbsent(name, k -> objectFactory.getObject());
    }
    
    // Удаляет объект из scope
    public Object remove(String name) {
        return beans.remove(name);
    }
    
    // Регистрирует callback для разрушения объекта
    public void registerDestructionCallback(String name, Runnable callback) {
        // Можно сохранить callback и вызвать при необходимости
    }
    
    // Возвращает идентификатор scope сессии
    public Object getConversationId() {
        return Thread.currentThread().getId();
    }
    
    // Очистка scope
    public void clear() {
        beans.clear();
    }
}

Шаг 2: Регистрируем Scope в контейнере

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ScopeConfiguration {
    
    @Bean
    public static CustomScopeConfigurer customScopeConfigurer() {
        CustomScopeConfigurer configurer = new CustomScopeConfigurer();
        configurer.addScope("tenant", new TenantScope());
        return configurer;
    }
}

Шаг 3: Используем custom scope

@Component
@Scope("tenant")  // Используем наш собственный scope
public class TenantData {
    private String tenantId;
    
    public String getTenantId() {
        return tenantId;
    }
    
    public void setTenantId(String tenantId) {
        this.tenantId = tenantId;
    }
}

@Service
public class MultiTenantService {
    private final TenantData tenantData;
    
    public MultiTenantService(TenantData tenantData) {
        this.tenantData = tenantData;
    }
    
    public void processTenant(String tenantId) {
        tenantData.setTenantId(tenantId);
        System.out.println("Processing for tenant: " + tenantData.getTenantId());
    }
}

Пример: Thread-Local Scope

public class ThreadLocalScope implements Scope {
    private final ThreadLocal<Map<String, Object>> threadLocal = 
        ThreadLocal.withInitial(HashMap::new);
    
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Map<String, Object> scopedObjects = threadLocal.get();
        return scopedObjects.computeIfAbsent(name, k -> objectFactory.getObject());
    }
    
    public Object remove(String name) {
        Map<String, Object> scopedObjects = threadLocal.get();
        return scopedObjects.remove(name);
    }
    
    public void registerDestructionCallback(String name, Runnable callback) {
        // При необходимости вызываем callback
    }
    
    public Object getConversationId() {
        return Thread.currentThread().getId();
    }
    
    public void clear() {
        threadLocal.remove();
    }
}

// Конфигурация
@Configuration
public class ThreadScopeConfig {
    @Bean
    public static CustomScopeConfigurer customScopeConfigurer() {
        CustomScopeConfigurer configurer = new CustomScopeConfigurer();
        configurer.addScope("threadLocal", new ThreadLocalScope());
        return configurer;
    }
}

// Использование
@Component
@Scope("threadLocal")
public class ThreadContext {
    private String userId;
    public String getUserId() { return userId; }
    public void setUserId(String userId) { this.userId = userId; }
}

Пример: Request-Context Scope (более продвинутый)

public class RequestContextScope implements Scope {
    private final Map<String, Object> beans = new ConcurrentHashMap<>();
    private final Map<String, List<Runnable>> destructionCallbacks = new ConcurrentHashMap<>();
    
    public Object get(String name, ObjectFactory<?> objectFactory) {
        return beans.computeIfAbsent(name, k -> objectFactory.getObject());
    }
    
    public Object remove(String name) {
        List<Runnable> callbacks = destructionCallbacks.remove(name);
        if (callbacks != null) {
            callbacks.forEach(Runnable::run);
        }
        return beans.remove(name);
    }
    
    public void registerDestructionCallback(String name, Runnable callback) {
        destructionCallbacks.computeIfAbsent(name, k -> new ArrayList<>()).add(callback);
    }
    
    public Object getConversationId() {
        return UUID.randomUUID();
    }
    
    // Разрушаем все бины scope
    public void destroy() {
        destructionCallbacks.values().forEach(callbacks -> 
            callbacks.forEach(Runnable::run)
        );
        beans.clear();
        destructionCallbacks.clear();
    }
}

Практический пример: Web-Context Scope

public class WebContextScope implements Scope {
    private static final ThreadLocal<Map<String, Object>> contextHolder = 
        ThreadLocal.withInitial(HashMap::new);
    
    public static void setContext(Map<String, Object> context) {
        contextHolder.set(context);
    }
    
    public static void clearContext() {
        contextHolder.remove();
    }
    
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Map<String, Object> context = contextHolder.get();
        return context.computeIfAbsent(name, k -> objectFactory.getObject());
    }
    
    public Object remove(String name) {
        return contextHolder.get().remove(name);
    }
    
    public void registerDestructionCallback(String name, Runnable callback) {}
    
    public Object getConversationId() {
        return Thread.currentThread().getId();
    }
}

// Конфигурация
@Configuration
public class WebContextConfig {
    @Bean
    public static CustomScopeConfigurer customScopeConfigurer() {
        CustomScopeConfigurer configurer = new CustomScopeConfigurer();
        configurer.addScope("webContext", new WebContextScope());
        return configurer;
    }
}

// Использование в контроллере
@RestController
@RequestMapping("/api")
public class UserController {
    @GetMapping("/user/{id}")
    public ResponseEntity<UserDTO> getUser(@PathVariable String id) {
        WebContextScope.setContext(new HashMap<>(Map.of(
            "userId", id,
            "timestamp", System.currentTimeMillis()
        )));
        
        try {
            UserDTO user = userService.getUser(id);
            return ResponseEntity.ok(user);
        } finally {
            WebContextScope.clearContext();
        }
    }
}

@Component
@Scope("webContext")
public class UserContext {
    private String userId;
    private long timestamp;
    
    public String getUserId() { return userId; }
    public void setUserId(String userId) { this.userId = userId; }
}

Когда создавать custom scope?

СценарийРешение
HTTP запрос@Scope("request")
HTTP сессия@Scope("session")
Один на весь app@Scope("singleton")
Новый каждый раз@Scope("prototype")
Multi-tenant приложениеCustom Scope
Thread-specific данныеCustom Scope (ThreadLocal)
Время жизни = ContextCustom Scope

Best Practices

// Всегда регистрируйте destruction callbacks
public void registerDestructionCallback(String name, Runnable callback) {
    // Важно для очистки ресурсов
    destructionCallbacks.put(name, callback);
}

// Используйте thread-safe коллекции
private final Map<String, Object> beans = new ConcurrentHashMap<>();

// Документируйте время жизни
@Component
@Scope("custom")
// Время жизни: завязано на requestId, разрушается в конце обработки
public class RequestScopedService {}

// Предпочитайте composition вместо наследования
public class CustomScope implements Scope {
    // Лучше компонировать, чем расширять
}

Выводы

  1. Custom Scope мощный инструмент для специальных сценариев
  2. Реализуется через интерфейс Scope с методами get/remove
  3. Регистрируется через CustomScopeConfigurer
  4. Полезен для multi-tenancy, request context, thread context
  5. Требует аккуратности с управлением ресурсами и очисткой
Можно ли создать свой Scope бинов? | PrepBro