15 Übung: Dependency Injection in Aktion

Theorie ist verdaut. Jetzt wird es praktisch. In dieser Übung baust du die erste echte Service-Schicht für das Arcade-Projekt.

15.1 Ziel

Am Ende dieser Übung hast du:

15.2 Schritt 1: Das Interface definieren

Erstelle ein neues Package repository und darin das Interface:

src/main/java/de/digitalfrontiers/arcade/repository/GameRepository.java

package de.digitalfrontiers.arcade.repository;

import de.digitalfrontiers.arcade.domain.Game;
import java.util.List;
import java.util.Optional;

public interface GameRepository {

    List<Game> findAll();

    Optional<Game> findById(String id);

    Game save(Game game);
}

Das Interface definiert den Vertrag. Wie die Daten gespeichert werden, ist hier egal.

15.3 Schritt 2: Die In-Memory-Implementierung

Für den Anfang speichern wir alles im Speicher. Später wird das durch MongoDB ersetzt – der Service merkt davon nichts.

src/main/java/de/digitalfrontiers/arcade/repository/InMemoryGameRepository.java

package de.digitalfrontiers.arcade.repository;

import de.digitalfrontiers.arcade.domain.Game;
import org.springframework.stereotype.Repository;

import jakarta.annotation.PostConstruct;
import java.util.*;

@Repository
public class InMemoryGameRepository implements GameRepository {

    private final Map<String, Game> games = new HashMap<>();

    @PostConstruct
    public void initializeGames() {
        // Startdaten für die Arcade
        save(new Game("pacman", "Pac-Man",
                "Friss alle Punkte, meide die Geister", 3333360));
        save(new Game("tetris", "Tetris",
                "Stapel die Blöcke, räume Reihen ab", 999999));
        save(new Game("snake", "Snake",
                "Wachse, ohne dich selbst zu beißen", 99999));
        save(new Game("space-invaders", "Space Invaders",
                "Vernichte die Alien-Invasion", 99990));
    }

    @Override
    public List<Game> findAll() {
        return new ArrayList<>(games.values());
    }

    @Override
    public Optional<Game> findById(String id) {
        return Optional.ofNullable(games.get(id));
    }

    @Override
    public Game save(Game game) {
        games.put(game.getId(), game);
        return game;
    }
}

Beachte: - @Repository markiert die Klasse als Bean - @PostConstruct füllt initiale Testdaten ein - Die Implementierung ist simpel – das ist Absicht

15.4 Schritt 3: Der Service

Jetzt der Service, der das Repository nutzt:

src/main/java/de/digitalfrontiers/arcade/service/GameService.java

package de.digitalfrontiers.arcade.service;

import de.digitalfrontiers.arcade.domain.Game;
import de.digitalfrontiers.arcade.repository.GameRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class GameService {

    private final GameRepository gameRepository;

    public GameService(GameRepository gameRepository) {
        this.gameRepository = gameRepository;
    }

    public List<Game> getAllGames() {
        return gameRepository.findAll();
    }

    public Game getGame(String id) {
        return gameRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException(
                        "Game not found: " + id));
    }
}

Das ist Constructor Injection: - Das Feld ist final - Der Konstruktor nimmt das Interface entgegen - Spring injiziert die InMemoryGameRepository-Bean

15.5 Schritt 4: Domain-Klasse anpassen

Die Game-Klasse braucht einen Konstruktor mit allen Feldern:

src/main/java/de/digitalfrontiers/arcade/domain/Game.java

package de.digitalfrontiers.arcade.domain;

public class Game {

    private String id;
    private String name;
    private String description;
    private Integer maxScore;

    public Game() {}

    public Game(String id, String name, String description, Integer maxScore) {
        this.id = id;
        this.name = name;
        this.description = description;
        this.maxScore = maxScore;
    }

    // Getter und Setter
    public String getId() { return id; }
    public void setId(String id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }

    public Integer getMaxScore() { return maxScore; }
    public void setMaxScore(Integer maxScore) { this.maxScore = maxScore; }
}

15.6 Schritt 5: Controller erweitern

Der HomeController soll die Spiele anzeigen. Erweitere ihn:

src/main/java/de/digitalfrontiers/arcade/web/HomeController.java

package de.digitalfrontiers.arcade.web;

import de.digitalfrontiers.arcade.service.GameService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

    private final GameService gameService;

    public HomeController(GameService gameService) {
        this.gameService = gameService;
    }

    @GetMapping("/")
    public String home(Model model) {
        model.addAttribute("games", gameService.getAllGames());
        return "index";
    }
}

Auch hier: Constructor Injection mit final-Feld.

15.7 Schritt 6: Testen

Starte die Anwendung:

./gradlew bootRun

In der Konsole solltest du keine Fehler sehen. Spring hat:

  1. InMemoryGameRepository gefunden und instanziiert
  2. GameService gefunden und InMemoryGameRepository injiziert
  3. HomeController gefunden und GameService injiziert

Die Kette funktioniert.

15.8 Was hier passiert ist

Spring hat die Abhängigkeitskette automatisch aufgelöst. Du hast nirgends new GameService(...) geschrieben. Das ist Dependency Injection.

15.9 Bonus: Die Injection sichtbar machen

Füge Logging hinzu, um die Injection zu sehen:

@Service
public class GameService {

    private static final Logger log = LoggerFactory.getLogger(GameService.class);

    private final GameRepository gameRepository;

    public GameService(GameRepository gameRepository) {
        this.gameRepository = gameRepository;
        log.info("GameService created with repository: {}",
                gameRepository.getClass().getSimpleName());
    }
}

Beim Start siehst du:

GameService created with repository: InMemoryGameRepository

Der Service weiß nicht, dass er InMemoryGameRepository bekommt – er sieht nur das Interface. Aber das Log zeigt, was Spring tatsächlich injiziert hat.