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

Какие знаешь типы Bean?

2.0 Middle🔥 221 комментариев
#Spring Boot и Spring Data#Spring Framework

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

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

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

Типы Spring Bean (Области видимости)

Bean — это объект, создаваемый и управляемый Spring контейнером. Spring предоставляет несколько типов (scopes) с разным жизненным циклом и поведением. Это фундаментальная концепция Spring Framework.

1. Singleton Scope (По умолчанию)

Один экземпляр на весь контекст приложения. Самый часто используемый.

@Configuration
public class SingletonBeanConfig {
    @Bean
    public UserRepository userRepository() {
        return new UserRepository();
    }
}

// Или через @Component
@Service
public class UserService {
    // Создаётся один раз при запуске приложения
}

// Использование
@RestController
public class UserController {
    @Autowired
    private UserService service; // один и тот же экземпляр
}

// Проверка того же экземпляра
@Test
public void testSingletonScope() {
    UserService service1 = context.getBean(UserService.class);
    UserService service2 = context.getBean(UserService.class);
    
    assertTrue(service1 == service2); // true — один объект
}

Преимущества:

  • Минимальное использование памяти
  • Быстрая доступность (инициализирован один раз)
  • Хорошо для stateless сервисов

Недостатки:

  • Нет thread-safety по умолчанию
  • Сложнее тестировать
// ❌ Проблема с Singleton и состоянием
@Service
public class BadUserService {
    private User currentUser; // ОПАСНО! Shared state
    
    public void processUser(User user) {
        this.currentUser = user; // проблема при многопоточности
    }
}

// ✅ Правильно — stateless Singleton
@Service
public class GoodUserService {
    private final UserRepository repository;
    
    public void processUser(User user) {
        // не сохраняем состояние в полях
        repository.save(user);
    }
}

2. Prototype Scope

Новый экземпляр создаётся каждый раз при запросе.

@Configuration
public class PrototypeBeanConfig {
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public UserRequest userRequest() {
        return new UserRequest();
    }
}

// Или через @Component
@Component
@Scope("prototype")
public class UserRequest {
    private UUID requestId = UUID.randomUUID();
    
    public UUID getRequestId() {
        return requestId;
    }
}

// Использование
@Test
public void testPrototypeScope() {
    UserRequest request1 = context.getBean(UserRequest.class);
    UserRequest request2 = context.getBean(UserRequest.class);
    
    assertNotEquals(request1.getRequestId(), request2.getRequestId()); // true — разные объекты
}

// Инъекция Prototype в Singleton
@Service
public class UserService {
    @Autowired
    private UserRequest request; // Получит один и тот же prototype bean!
    // Это НЕ то, что ожидаем
}

// ✅ Правильное решение — Provider
@Service
public class UserService {
    @Autowired
    private Provider<UserRequest> requestProvider;
    
    public void processRequest() {
        UserRequest request = requestProvider.get(); // новый экземпляр каждый раз
    }
}

Преимущества:

  • Каждый запрос получает свою копию
  • Хорошо для stateful объектов
  • Нет проблем с многопоточностью

Недостатки:

  • Больше потребления памяти
  • Spring не управляет полным жизненным циклом

3. Request Scope

Один экземпляр на один HTTP запрос.

@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
    private UUID requestId = UUID.randomUUID();
    private long startTime = System.currentTimeMillis();
    
    public UUID getRequestId() {
        return requestId;
    }
    
    public long getElapsedTime() {
        return System.currentTimeMillis() - startTime;
    }
}

@RestController
public class UserController {
    @Autowired
    private RequestContext requestContext;
    
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable UUID id) {
        System.out.println("Request ID: " + requestContext.getRequestId());
        return userService.findById(id);
    }
}

// Каждый HTTP запрос получает свой RequestContext
// GET /users/1 → RequestContext с id1
// GET /users/2 → RequestContext с id2

Проксирование (ScopedProxyMode):

// SCOPE_REQUEST требует прокси для работы в Singleton контексте
@Component
@Scope(
    value = WebApplicationContext.SCOPE_REQUEST,
    proxyMode = ScopedProxyMode.TARGET_CLASS // динамический прокси
)
public class RequestData {
    private String data;
}

// Spring создаёт CGLIB прокси
// Каждый вызов метода роутируется к правильному экземпляру для текущего запроса

Преимущества:

  • Идеально для request-specific данных
  • Автоматическое управление жизненным циклом

Недостатки:

  • Только в web приложениях
  • Overhead от прокси

4. Session Scope

Один экземпляр на HTTP сессию.

@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserSession {
    private User currentUser;
    private List<String> visitedPages = new ArrayList<>();
    private LocalDateTime createdAt = LocalDateTime.now();
    
    public void setCurrentUser(User user) {
        this.currentUser = user;
    }
    
    public User getCurrentUser() {
        return currentUser;
    }
    
    public void addVisitedPage(String page) {
        visitedPages.add(page);
    }
    
    public List<String> getVisitedPages() {
        return visitedPages;
    }
}

@RestController
public class UserController {
    @Autowired
    private UserSession userSession;
    
    @GetMapping("/profile")
    public UserProfileDTO getProfile() {
        userSession.addVisitedPage("/profile");
        User user = userSession.getCurrentUser();
        return mapToDTO(user);
    }
    
    @GetMapping("/history")
    public List<String> getHistory() {
        return userSession.getVisitedPages();
    }
}

// Разные пользователи получают разные SessionBeans

Преимущества:

  • Хранение состояния пользователя между запросами
  • Встроенная сериализация для persistence

Недостатки:

  • Потребление памяти
  • Сложность в распределённых системах

5. Application Scope

Один экземпляр на весь жизненный цикл ServletContext.

@Component
@Scope(WebApplicationContext.SCOPE_APPLICATION)
public class ApplicationMetrics {
    private long totalRequests = 0;
    private long startTime = System.currentTimeMillis();
    private final Map<String, Long> endpointCalls = new ConcurrentHashMap<>();
    
    public void recordRequest(String endpoint) {
        totalRequests++;
        endpointCalls.merge(endpoint, 1L, Long::sum);
    }
    
    public Map<String, Object> getMetrics() {
        return Map.of(
            "totalRequests", totalRequests,
            "uptime", System.currentTimeMillis() - startTime,
            "endpointCalls", endpointCalls
        );
    }
}

@RestController
public class MetricsController {
    @Autowired
    private ApplicationMetrics metrics;
    
    @GetMapping("/metrics")
    public Map<String, Object> getMetrics() {
        return metrics.getMetrics();
    }
}

6. WebSocket Scope

Один экземпляр на WebSocket сессию.

@Component
@Scope("websocket")
public class WebSocketSession {
    private String sessionId = UUID.randomUUID().toString();
    private List<Message> messages = new ArrayList<>();
    
    public void addMessage(Message msg) {
        messages.add(msg);
    }
}

@Component
public class WebSocketHandler implements WebSocketHandler {
    @Autowired
    private WebSocketSession wsSession; // один на WebSocket соединение
    
    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) 
            throws Exception {
        wsSession.addMessage(new Message(message.getPayload()));
    }
}

7. Custom Scope

Для специальных нужд можем создать свой scope.

public class ThreadScopeRegistry implements Scope {
    private static final ThreadLocal<Map<String, Object>> threadBeans = 
        ThreadLocal.withInitial(HashMap::new);
    
    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Map<String, Object> beanMap = threadBeans.get();
        
        if (!beanMap.containsKey(name)) {
            beanMap.put(name, objectFactory.getObject());
        }
        
        return beanMap.get(name);
    }
    
    @Override
    public Object remove(String name) {
        return threadBeans.get().remove(name);
    }
    
    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        // нужно реализовать если требуется cleanup
    }
    
    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }
    
    @Override
    public String getConversationId() {
        return Thread.currentThread().getName();
    }
}

@Configuration
public class CustomScopeConfig {
    @Bean
    public static CustomScopeConfigurer customScopeConfigurer() {
        CustomScopeConfigurer configurer = new CustomScopeConfigurer();
        configurer.addScope("thread", new ThreadScopeRegistry());
        return configurer;
    }
}

// Использование
@Component
@Scope("thread")
public class ThreadLocalBean {
    // один экземпляр на каждый поток
}

Сравнение типов Bean

ScopeСозданиеЖизненный циклИспользованиеThread-safe
Singleton1 разВсё приложениеСервисы, репозитории✓ (если stateless)
PrototypeКаждый разДо garbage collectionStateful объекты
RequestОдин на запросОдин HTTP запросRequest-specific данные
SessionОдин на сессиюСессия пользователяПользовательское состояние
Application1 разЖизненный цикл приложенияГлобальные метрики✓ (при правильном использовании)
WebSocketОдин на соединениеЖизненный цикл соединенияWebSocket коммуникация

Лучшие практики

// ✅ Singleton (по умолчанию) для stateless сервисов
@Service
public class UserService {
    private final UserRepository repository;
    
    // Зависимости через constructor
    public UserService(UserRepository repository) {
        this.repository = repository;
    }
}

// ✅ Prototype для stateful объектов
@Component
@Scope("prototype")
public class ReportBuilder {
    private List<ReportLine> lines = new ArrayList<>();
    
    public void addLine(ReportLine line) {
        lines.add(line);
    }
}

// ❌ Singleton с состоянием
@Service
public class BadService {
    private List<User> users; // общее состояние — проблема!
}

// ✅ Request scope для контекста запроса
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, 
       proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestInfo {
    private UUID requestId = UUID.randomUUID();
}

Проксирование (ScopedProxyMode)

// ScopedProxyMode.NO (по умолчанию)
// Spring создаёт простой объект

// ScopedProxyMode.TARGET_CLASS
// Spring создаёт CGLIB прокси класса
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)

// ScopedProxyMode.INTERFACES  
// Spring создаёт JDK прокси интерфейса
@Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
public interface RequestData {
    String getData();
}

Заключение

  • Singleton — 80% случаев (сервисы, репозитории)
  • Prototype — для stateful объектов
  • Request/Session — для веб-приложений
  • Избегай состояния в Singleton beans
  • Всегда используй dependency injection через конструктор

Правильный выбор scope критичен для масштабируемости и надёжности приложения.

Какие знаешь типы Bean? | PrepBro