Без IoC класс сам создаёт зависимости: new JdbcOrderRepository(new HikariDataSource(...)). Он знает о конкретных классах, их конфигурации и порядке создания; подменить базу в тесте сложно.
Инверсия управления — отдать эту ответственность внешнему коду: фреймворк создаёт объекты, связывает их и вызывает ваш код в нужный момент («не звоните нам, мы позвоним вам»).
Внедрение зависимостей — способ реализации: класс объявляет, что ему нужно (параметры конструктора), а контейнер передаёт готовые объекты.
@Service
class OrderService {
private final OrderRepository repo;
OrderService(OrderRepository repo) { this.repo = repo; } // зависимость передана извне
}
Что даёт: класс зависит от абстракции, а не от реализации; в тесте передаётся заглушка без запуска контекста; конфигурация (какая реализация, какие параметры) собрана в одном месте; контейнер управляет жизненным циклом — singleton, закрытие ресурсов.
Способы внедрения: конструктор (рекомендуемый), сеттер (для необязательных зависимостей), поле (@Autowired на поле — удобно, но плохо для тестов и неизменяемости).