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

В чем разница между init() в бинах Singleton и Prototype?

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

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

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

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

Разница между init() в Singleton и Prototype бинах

Это важный вопрос, касающийся жизненного цикла Spring бинов и того, как @PostConstruct / init() методы ведут себя по-разному в зависимости от scope бина. Разница существенна и может привести к утечкам ресурсов или неожиданному поведению.

Основные отличия

Singleton scope (default)

import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

// По умолчанию scope = Singleton
@Component
public class SingletonService {
    
    @PostConstruct
    public void init() {
        System.out.println("SingletonService initialized");
        // Выполнится ОДИН РАЗ при создании бина
        // (обычно при запуске приложения)
    }
    
    @PreDestroy
    public void destroy() {
        System.out.println("SingletonService destroyed");
        // Выполнится ОДИН РАЗ при завершении приложения
    }
    
    public void doSomething() {
        System.out.println("Doing something");
    }
}

// Использование
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Application.class, args);
        
        // Получение бина первый раз
        SingletonService service1 = context.getBean(SingletonService.class);
        // Вывод: "SingletonService initialized"
        service1.doSomething();
        
        // Получение бина второй раз - это ТОТ ЖЕ объект
        SingletonService service2 = context.getBean(SingletonService.class);
        System.out.println(service1 == service2);  // true
        
        // Контекст закрывается
        context.close();
        // Вывод: "SingletonService destroyed"
    }
}

Характеристики Singleton:

  • init() вызывается один раз при создании бина
  • Обычно при старте приложения (eager initialization)
  • destroy() вызывается один раз при закрытии контекста
  • Один экземпляр на весь Spring контекст
  • Часть стандартного lifecycle контекста

Prototype scope

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PrototypeService {
    
    @PostConstruct
    public void init() {
        System.out.println("PrototypeService initialized");
        // Выполнится КАЖДЫЙ РАЗ при создании нового экземпляра
    }
    
    @PreDestroy
    public void destroy() {
        System.out.println("PrototypeService destroyed");
        // МОЖЕТ НЕ БЫТЬ ВЫЗВАН! (зависит от контейнера)
    }
    
    public void doSomething() {
        System.out.println("Doing something");
    }
}

// Использование
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Application.class, args);
        
        // Получение бина первый раз
        PrototypeService service1 = context.getBean(PrototypeService.class);
        // Вывод: "PrototypeService initialized"
        service1.doSomething();
        
        // Получение бина второй раз - это НОВЫЙ объект!
        PrototypeService service2 = context.getBean(PrototypeService.class);
        // Вывод: "PrototypeService initialized" (еще раз)
        System.out.println(service1 == service2);  // false - разные объекты!
        
        // Контекст закрывается
        context.close();
        // НЕ выводит "PrototypeService destroyed"
        // Spring не управляет lifecycle Prototype бинов!
    }
}

Характеристики Prototype:

  • init() вызывается для каждого нового экземпляра
  • Может быть создан много раз во время работы приложения
  • destroy() НЕ вызывается Spring контейнером
  • Новый экземпляр создается при каждом getBean() вызове
  • Управление жизненным циклом — ответственность клиента

Сравнительная таблица

АспектSingletonPrototype
Количество инстанций1 на контекстМного (каждый запрос)
init() вызовов1 раз при созданииКаждый раз при getBean()
destroy() вызовов1 раз при shutdown0 раз (не контролируется Spring)
Управление жизненным цикломSpring управляетКлиент управляет
ПроизводительностьОптимально (переиспользование)Дороже (создание объектов)
ПотокобезопасностьПотокобезопасные операции нужныНет проблем (отдельный экземпляр)
Когда использоватьStateless сервисыStateful объекты

Практические примеры

Пример 1: Singleton с ресурсами

@Component
public class DatabaseConnection {
    private Connection connection;
    
    @PostConstruct
    public void init() {
        try {
            this.connection = DriverManager.getConnection("jdbc:mysql://localhost/db");
            System.out.println("Database connected ONCE");
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
    
    @PreDestroy
    public void destroy() {
        try {
            if (connection != null) {
                connection.close();
                System.out.println("Database disconnected");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    
    public void executeQuery(String sql) {
        try {
            connection.createStatement().execute(sql);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

// Использование
@Service
public class UserService {
    @Autowired
    private DatabaseConnection db;
    
    public void getAllUsers() {
        // Использует ту же базовую连接 (singleton)
        db.executeQuery("SELECT * FROM users");
    }
}

Пример 2: Prototype для request-scoped объектов

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class RequestContext {
    private String requestId;
    private LocalDateTime createdAt;
    private Map<String, Object> attributes = new HashMap<>();
    
    @PostConstruct
    public void init() {
        this.requestId = UUID.randomUUID().toString();
        this.createdAt = LocalDateTime.now();
        System.out.println("RequestContext created: " + requestId);
    }
    
    public void setAttribute(String key, Object value) {
        attributes.put(key, value);
    }
    
    public Object getAttribute(String key) {
        return attributes.get(key);
    }
    
    public String getRequestId() {
        return requestId;
    }
}

// Использование в Controller
@RestController
public class UserController {
    @Autowired
    private RequestContext requestContext;  // Каждый запрос = новый объект
    
    @GetMapping("/users")
    public List<User> getUsers() {
        System.out.println("Request ID: " + requestContext.getRequestId());
        // Каждый HTTP запрос получает новый RequestContext
        return userService.findAll();
    }
}

Важная проблема: destroy() в Prototype

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PrototypeWithResources {
    private FileInputStream fileStream;
    
    @PostConstruct
    public void init() throws FileNotFoundException {
        this.fileStream = new FileInputStream("myfile.txt");
        System.out.println("FileInputStream opened");
    }
    
    @PreDestroy
    public void destroy() {
        try {
            if (fileStream != null) {
                fileStream.close();
                System.out.println("FileInputStream closed");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// ПРОБЛЕМА!
@Service
public class MyService {
    @Autowired
    private ApplicationContext context;
    
    public void processFiles() {
        for (int i = 0; i < 1000; i++) {
            PrototypeWithResources proto = context.getBean(PrototypeWithResources.class);
            // init() вызывается -> FileInputStream открывается
            proto.doSomething();
            // destroy() НЕ вызывается -> утечка файловых дескрипторов!
        }
    }
}

Решение: явное управление жизненным циклом

@Service
public class MyService {
    @Autowired
    private ApplicationContext context;
    
    public void processFiles() {
        for (int i = 0; i < 1000; i++) {
            PrototypeWithResources proto = context.getBean(PrototypeWithResources.class);
            try {
                proto.doSomething();
            } finally {
                // Явное управление
                if (proto instanceof DisposableBean) {
                    ((DisposableBean) proto).destroy();
                }
            }
        }
    }
}

// Или используй try-with-resources
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PrototypeWithResources implements AutoCloseable {
    private FileInputStream fileStream;
    
    @PostConstruct
    public void init() throws FileNotFoundException {
        this.fileStream = new FileInputStream("myfile.txt");
    }
    
    @Override
    public void close() throws Exception {
        if (fileStream != null) {
            fileStream.close();
        }
    }
}

// Использование
public void processFiles() {
    for (int i = 0; i < 1000; i++) {
        try (PrototypeWithResources proto = context.getBean(PrototypeWithResources.class)) {
            proto.doSomething();
        } // close() вызывается автоматически
    }
}

Request и Session scopes в Web

// REQUEST scope - создается для каждого HTTP запроса
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestScopedService {
    @PostConstruct
    public void init() {
        System.out.println("Request scope bean initialized");
    }
    
    @PreDestroy
    public void destroy() {
        System.out.println("Request scope bean destroyed (при завершении запроса)");
    }
}

// SESSION scope - создается для каждой HTTP сессии
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionScopedService {
    @PostConstruct
    public void init() {
        System.out.println("Session scope bean initialized");
    }
    
    @PreDestroy
    public void destroy() {
        System.out.println("Session scope bean destroyed (при завершении сессии)");
    }
}

Жизненный цикл Request/Session:

  • init() вызывается один раз при создании бина
  • destroy() вызывается один раз при завершении request/session
  • Spring УПРАВЛЯЕТ жизненным циклом (в отличие от Prototype)

Best Practices

  1. Singleton для stateless сервисов:

    @Component
    public class UserService {  // Singleton по default
        public User findById(Long id) { ... }
    }
    
  2. Prototype для stateful объектов:

    @Component
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public class UserRequest {
        private String userId;
        private List<String> roles = new ArrayList<>();
    }
    
  3. Request/Session scope в Web приложениях:

    @Component
    @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
    public class CurrentUser {
        private Long userId;
    }
    
  4. Явное управление ресурсами в Prototype:

    // Используй try-with-resources или явное close()
    
  5. Помни о потокобезопасности Singleton:

    // Singleton используется из разных потоков
    // Убедись, что операции потокобезопасные!
    

Заключение

Главное отличие: Singleton имеет одну init() при создании и одну destroy() при shutdown, а Prototype имеет init() для каждого экземпляра и НЕ имеет гарантированного destroy(). Выбор scope зависит от природы объекта (stateless vs stateful) и требований приложения. Неправильный выбор может привести к утечкам ресурсов или неожиданным race conditions.

В чем разница между init() в бинах Singleton и Prototype? | PrepBro