УРОК 2 / 30 0%

🏗 Архитектура слоёв + Spring Core

🎯

Цель урока

Понять, почему логика НЕ в Controller, что такое Bean, IoC, DI, и разложить TaskFlow по слоям: controller → service → repository.

🧠

Теория · для собеса

1 Слои Spring Boot-приложения
HTTP request
   ↓
[Filter chain]        ← логирование, безопасность, JWT
   ↓
[DispatcherServlet]   ← «маршрутизатор» Spring MVC
   ↓
[Controller]          ← @RestController, принимает запрос
   ↓
[Service]             ← @Service, бизнес-логика
   ↓
[Repository]          ← @Repository, доступ к БД
   ↓
[Database]

Почему нельзя писать всю логику в Controller:

  • Controller = «входная дверь». Должен быть тонким.
  • Если вся логика в нём — его невозможно переиспользовать (другой endpoint, scheduler, Kafka consumer, тест).
  • Service-слой не знает про HTTP → его можно вызвать откуда угодно.
  • Service легко покрыть unit-тестами (Controller требует MockMvc / WebMvcTest).
💡
На собесе: «почему Service отдельно от Controller?» — «чтобы переиспользовать логику и тестировать без HTTP».
2 Что такое Bean, IoC, DI

Bean — это объект, который создаёт и хранит IoC-контейнер Spring.

IoC (Inversion of Control) — принцип: не ты создаёшь объекты через new, а контейнер Spring создаёт их за тебя и сам подсовывает куда нужно.

DI (Dependency Injection) — способ, которым Spring «подсовывает» зависимости:

@Service
public class TaskService {
    private final TaskRepository repo;

    // Constructor injection — Spring ВЫЗЫВАЕТ этот конструктор
    public TaskService(TaskRepository repo) {
        this.repo = repo;
    }
}
🧠 Аналогия: обычный код — ты сам идёшь на склад, ищешь инструмент, несёшь в цех. Spring IoC — склад сам привозит инструменты к тебе в цех, когда ты начал работу.
💡
На собесе: «IoC vs DI» — IoC это принцип, DI это способ реализации.
3 3 способа инъекции (constructor — лучший)
Способ
Проблема
Constructor ✅
нет — компилятор гарантирует
Setter
можно создать без зависимости → NPE
Field @Autowired
нельзя final, плохо для тестов, скрытая зависимость

Правило: всегда constructor injection. Поля помечай final.

💡
На собесе: «почему constructor лучше field injection?» — «final, тестируемость, явность».
4 Аннотации Spring (минимум)
@Component

обобщённый класс, создаётся контейнером

@Service

то же + семантика «бизнес-логика»

@Repository

то же + семантика «доступ к БД» (+ перехват исключений JPA)

@RestController

@Controller + @ResponseBody на каждом методе (возвращает данные)

@Configuration

источник бинов (через @Bean-методы)

@SpringBootApplication

= @Configuration + @EnableAutoConfiguration + @ComponentScan

5 Что делает @ComponentScan?

Сканирует пакет, где лежит @SpringBootApplication, и все его подпакеты. Все классы с @Component / @Service / @Repository / @RestController / @Configuration внутри становятся бинами.

⚠️
Важно: если класс TaskService лежит в com.taskflow.service, а @SpringBootApplication в com.taskflow — всё ОК (подпакет). Если класс лежит в com.example.service (параллельная ветка) — Spring его не увидит.
🗺 Аналогия: @ComponentScan — это «смотри в папку com.taskflow и всё, что внутри». Всё, что лежит в соседней папке com.example — не его территория.
💻

Практика: разносим урок 1 по слоям

  1. 1
    Структура после рефакторинга
    src/main/java/com/taskflow/
    ├── TaskFlowApplication.java
    ├── controller/
    │   └── TaskController.java     # ТОЛЬКО HTTP
    ├── service/
    │   └── TaskService.java        # бизнес-логика
    ├── repository/
    │   └── TaskRepository.java     # хранение (пока List)
    ├── model/
    │   └── Task.java               # record с полями
    └── dto/
        └── TaskRequest.java        # record для входящих
  2. 2
    model/Task.java · record
    package com.taskflow.model;
    
    public record Task(
        Long id,
        String title,
        String description,
        boolean done
    ) {}
  3. 3
    repository/TaskRepository.java · in-memory
    package com.taskflow.repository;
    
    import com.taskflow.model.Task;
    import org.springframework.stereotype.Repository;
    import java.util.*;
    import java.util.concurrent.atomic.AtomicLong;
    
    @Repository
    public class TaskRepository {
        private final List<Task> tasks = new ArrayList<>();
        private final AtomicLong idGen = new AtomicLong(1);
    
        public List<Task> findAll() { return List.copyOf(tasks); }
        public Optional<Task> findById(Long id) {
            return tasks.stream().filter(t -> t.id().equals(id)).findFirst();
        }
        public Task save(Task task) {
            if (task.id() == null) {
                Task t = new Task(idGen.getAndIncrement(),
                                  task.title(), task.description(), task.done());
                tasks.add(t);
                return t;
            }
            tasks.removeIf(t -> t.id().equals(task.id()));
            tasks.add(task);
            return task;
        }
        public void deleteById(Long id) { tasks.removeIf(t -> t.id().equals(id)); }
    }
  4. 4
    service/TaskService.java · бизнес-логика
    package com.taskflow.service;
    
    import com.taskflow.model.Task;
    import com.taskflow.repository.TaskRepository;
    import org.springframework.stereotype.Service;
    import java.util.*;
    
    @Service
    public class TaskService {
        private final TaskRepository repo;
    
        public TaskService(TaskRepository repo) {   // constructor injection
            this.repo = repo;
        }
    
        public List<Task> findAll()        { return repo.findAll(); }
        public Optional<Task> findById(Long id) { return repo.findById(id); }
        public Task create(Task t)          { return repo.save(t); }
        public Task update(Long id, Task t) { return repo.save(new Task(id, t.title(), t.description(), t.done())); }
        public void delete(Long id)         { repo.deleteById(id); }
    }
  5. 5
    dto/TaskRequest.java · что приходит снаружи
    package com.taskflow.dto;
    
    public record TaskRequest(String title, String description, boolean done) {}
  6. 6
    controller/TaskController.java · только HTTP
    package com.taskflow.controller;
    
    import com.taskflow.dto.TaskRequest;
    import com.taskflow.model.Task;
    import com.taskflow.service.TaskService;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    import java.util.List;
    
    @RestController
    @RequestMapping("/api/tasks")
    public class TaskController {
        private final TaskService service;
    
        public TaskController(TaskService service) { this.service = service; }
    
        @GetMapping
        public List<Task> findAll() { return service.findAll(); }
    
        @GetMapping("/{id}")
        public ResponseEntity<Task> findById(@PathVariable Long id) {
            return service.findById(id).map(ResponseEntity::ok)
                                      .orElse(ResponseEntity.notFound().build());
        }
    
        @PostMapping
        public Task create(@RequestBody TaskRequest req) {
            return service.create(new Task(null, req.title(), req.description(), req.done()));
        }
    
        @PutMapping("/{id}")
        public Task update(@PathVariable Long id, @RequestBody TaskRequest req) {
            return service.update(id, new Task(id, req.title(), req.description(), req.done()));
        }
    
        @DeleteMapping("/{id}")
        public ResponseEntity<Void> delete(@PathVariable Long id) {
            service.delete(id);
            return ResponseEntity.noContent().build();
        }
    }
  7. 7
    Запуск и проверка
    ./gradlew bootRun
    
    # Создать
    $ curl -X POST http://localhost:8080/api/tasks \
      -H "Content-Type: application/json" \
      -d '{"title":"Buy milk","description":"2.5%","done":false}'
    {"id":1,"title":"Buy milk","description":"2.5%","done":false}
    
    # Получить все
    $ curl http://localhost:8080/api/tasks
    [{"id":1,"title":"Buy milk","description":"2.5%","done":false}]

    Логика разделена по слоям. 🎉

🎯

Зачем это на собесе

После урока ты должен уметь ответить на:

«Почему нельзя писать всю логику в Controller?» — не переиспользуется, плохо тестируется, нарушает SRP.
«IoC vs DI — в чём разница?» — IoC это принцип (контейнер управляет объектами), DI это способ реализации (через конструктор/сеттер/поле).
«Constructor vs field injection — что лучше?» — constructor: final, тестируемость, явность зависимостей.
«Чем @Service отличается от @Component?» — функционально ничем, но @Service несёт семантику «бизнес-логика» (полезно для AOP и аналитики).
«Что делает @ComponentScan?» — сканирует пакет с @SpringBootApplication и подпакеты, находит классы со стереотипными аннотациями.

5 вопросов на углубление

0 / 5 выбрано

Раскрой вопрос и нажми «🤔 Хочу разобрать подробнее» — он попадёт в страницу ответов.

1
Что будет, если в TaskService есть зависимость от TaskRepository, но в конструкторе не указан @Autowired?

С Spring 4.3+ аннотация @Autowired на единственном конструкторе необязательна. Что будет, если конструкторов несколько? Когда @Autowired обязателен?

2
Чем отличается Bean scope singleton (default) от prototype? Какой scope у Spring-бинов по умолчанию?

Singleton — один экземпляр на весь контекст. Prototype — новый при каждом запросе. Что с lifecycle-методами (@PreDestroy) у prototype?

3
Почему record подходит для Task, но НЕ подходит для @Entity в JPA?

Record иммутабельный. JPA требует no-arg constructor + сеттеры. Что ещё нужно JPA, чего record дать не может?

4
Что такое circular dependency? Как Spring её обнаруживает и как её решить?

A → B → A. Что бросает Spring? Когда @Lazy — лекарство, а когда — пластырь?

5
Почему в TaskRepository используется AtomicLong, а не long?

Race condition в многопоточном коде. Что такое CAS на уровне CPU?

Готов идти дальше?

Выбери вопросы, которые тебе интересны, и изучи их. Потом — к следующему уроку.

📖 Изучить выбранные вопросы

Сначала выбери хотя бы 1 вопрос на углубление

🚀 Перейти к Уроку 3

Сначала пройди все секции и выбери хотя бы 1 вопрос

⬅️ Назад к Уроку 1