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

Стоит ли по умолчанию Scope Singleton?

2.3 Middle🔥 281 комментариев
#Spring Framework

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

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

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

Spring Singleton Scope: Почему это по умолчанию

Вопрос: Стоит ли по умолчанию Singleton scope в Spring?

Ответ: Да, это правильный выбор по умолчанию для большинства компонентов.

Почему Singleton по умолчанию

1. Производительность

Singleton:
- Создание один раз при стартапе
- Один объект на всё приложение
- Быстрое получение из кеша

Prototype:
- Создание для КАЖДОГО запроса
- N объектов за сессию
- Медленнее и больше памяти

Метрика: для 1000 запросов
Singleton: 1 создание объекта
Prototype: 1000 созданий объектов

2. Контролируемое состояние

// ✓ Singleton работает если компонент stateless
@Service
public class UserService {
    @Autowired
    private UserRepository repository; // Dependence, не state
    
    public User findById(Long id) {
        return repository.findById(id);
    }
    // Один объект = безопасно
}

// ❌ Prototype нужен если есть state
@Service
@Scope("prototype")
public class StatefulService {
    private int counter; // State в объекте
    
    public void increment() {
        counter++; // Каждый запрос своё значение
    }
}

3. Инициализация при стартапе

// Singleton инициализируется один раз при стартапе
@Service
public class CacheService {
    @PostConstruct
    public void init() {
        // Загружаем кеш один раз
        loadAllDataIntoMemory();
    }
}

// Prototype создаёт новый объект каждый раз
// @PostConstruct вызывается ДЛЯ КАЖДОГО объекта
// Это неэффективно

Архитектура Spring приложения

Spring Container
│
├─ Service Layer (Singleton)
│  ├─ UserService
│  ├─ OrderService
│  └─ PaymentService
│
├─ Repository Layer (Singleton)
│  ├─ UserRepository
│  ├─ OrderRepository
│  └─ PaymentRepository
│
├─ Controller Layer (Singleton)
│  ├─ UserController
│  ├─ OrderController
│  └─ PaymentController
│
└─ Request-scoped beans (Request Scope)
   ├─ RequestContext
   └─ CurrentUserContext

Когда использовать другие Scope

RequestScope (для request-specific data)

@Component
@RequestScope // Новый на каждый HTTP request
public class RequestContext {
    private String userId;
    private String tenantId;
    private List<String> errors;
    
    // Getters/setters
}

@Service
public class UserService {
    @Autowired
    private RequestContext requestContext;
    
    public User getCurrentUser() {
        // Получить текущего пользователя из контекста
        String userId = requestContext.getUserId();
        return findById(userId);
    }
}

SessionScope (для session state)

@Component
@SessionScope // Новый на каждую сессию
public class UserSession {
    private User currentUser;
    private Locale locale;
    private List<Notification> notifications;
    
    // Getters/setters
}

// Использование в контроллере
@RestController
public class DashboardController {
    @Autowired
    private UserSession userSession;
    
    @GetMapping("/dashboard")
    public Dashboard getDashboard() {
        User user = userSession.getCurrentUser();
        // ...
    }
}

Prototype (редко, для stateful objects)

@Component
@Scope("prototype")
public class QueryBuilder {
    private String query = "SELECT ";
    
    public QueryBuilder withField(String field) {
        query += field + ", ";
        return this;
    }
    
    public String build() {
        return query + "FROM table";
    }
}

// Каждый запрос ориентирует новый объект
@Service
public class ReportService {
    @Autowired
    private QueryBuilder queryBuilder;
    
    public String generateQuery() {
        // Новый QueryBuilder для каждого вызова
        return queryBuilder
            .withField("id")
            .withField("name")
            .build();
    }
}

Проблема с Singleton при неправильном использовании

// ❌ ОПАСНО: Singleton с state
@Service
public class BadUserService {
    private List<User> cachedUsers; // ❌ Shared state!
    
    public void loadUsers() {
        this.cachedUsers = repository.findAll();
    }
    
    public List<User> getUsers() {
        return cachedUsers;
    }
}

// Проблемы:
// - Thread-safety issues если несколько потоков обращаются
// - Память: кеш растёт
// - Сложно тестировать

// ✓ ПРАВИЛЬНО: использовать внешний кеш
@Service
public class GoodUserService {
    @Autowired
    private UserRepository repository;
    
    @Autowired
    private RedisTemplate<String, User> redisTemplate; // Внешний кеш
    
    public User getUser(Long id) {
        return redisTemplate.opsForValue().get("user:" + id)
            .orElseGet(() -> repository.findById(id).orElse(null));
    }
}

Сравнение Scope

┌─────────────┬───────────┬────────────┬──────────────────┐
│ Scope       │ Создание  │ Параллели  │ Когда использовать│
├─────────────┼───────────┼────────────┼──────────────────┤
│ singleton   │ Один раз  │ Многие     │ Services, repos   │
│ prototype   │ Каждый    │ Один       │ Stateful (редко)  │
│ request     │ На request│ Один/request│ Request context   │
│ session     │ На session│ Один/session│ User session      │
│ application │ Один раз  │ All        │ Global config     │
└─────────────┴───────────┴────────────┴──────────────────┘

Singleton thread-safety

// Singleton ДОЛЖЕН быть thread-safe

// ✓ ПРАВИЛЬНО: Immutable
@Service
public class ImmutableService {
    private final ConfigProperties config;
    
    // Не меняется после инициализации
    @Autowired
    public ImmutableService(ConfigProperties config) {
        this.config = config;
    }
}

// ✓ ПРАВИЛЬНО: Stateless
@Service
public class StatelessService {
    @Autowired
    private UserRepository repository;
    
    public User find(Long id) {
        // Использует параметры, не field
        return repository.findById(id);
    }
}

// ✓ ПРАВИЛЬНО: Внешняя синхронизация
@Service
public class SynchronizedService {
    @Autowired
    private ConcurrentHashMap<String, Object> cache;
    
    // ConcurrentHashMap — thread-safe
}

На собеседовании

Правильный ответ:

"Да, Singleton scope по умолчанию — это правильное решение, потому что:

  1. Производительность: один объект на всё приложение, быстрое получение
  2. Память: экономия ресурсов
  3. Инициализация: один раз при стартапе
  4. Большинство компонентов stateless (Services, Repositories, Controllers)

Но это требует что компонент ДОЛЖЕН быть thread-safe:

  • Не должен хранить state в полях
  • Все зависимости через constructor injection
  • Если нужна request-specific data — использовать @RequestScope

Prototype scope редко нужен, потому что требует создания нового объекта для каждого использования, что медленно и дорого."

Best Practices

// ✓ Правильная архитектура
@Service
public class OrderService {
    @Autowired
    private OrderRepository repository; // Inject, не store
    
    @Autowired
    private RequestContext context; // Request-scoped
    
    public Order create(OrderRequest request) {
        // Используем параметры и injected зависимости
        String userId = context.getUserId();
        // ...
    }
}

Ключевые выводы

  • Singleton по умолчанию — правильно для большинства
  • Требует stateless компоненты — это обязательно
  • RequestScope для request-specific данных
  • SessionScope для user session данных
  • Prototype очень редко — почти никогда не нужен
  • Thread-safety — ответственность разработчика
  • Immutable или stateless — золотое правило