Sierpień 3, 2019

Spring Container – zarządzanie zależnościami

Spring Boot

W poprzednim artykule pokazałem Ci jak szybko można stworzyć projekt Spring Boot i go uruchomić. Było to bardzo proste, lecz nadszedł czas na najważniejszą część tej serii – zrozumienie magii Springa.

W tym artykule zajmę się omawianiem jednego z najistotniejszych modułów Springa – jest to Spring Container.

Zanim jednak przejdziemy dalej musisz być zaznajomiony z takim wzorcem projektowym jak dependency injection. Bez niego ani rusz, to m.in. na nim opiera się ten moduł.

Na szybko tylko przypomnę, że DI polega na tym, że zależności do klasy są wstrzykiwane z zewnątrz, a nie są bezpośrednio tworzone w środku klasy. Dzięki tak prostemu zabiegowi to klient klasy decyduje na jakich zależnościach klasa pracuje, dla klasy ważna jest tylko abstrakcja (spójny interfejs).

Przechodząc dalej robisz to na własną odpowiedzialność – bez znajomości dependency injection dalsze czytanie artykułu jest totalnie bez sensu.

Weź to pod uwagę.

Warsztat!

Nic tak mi się nie podoba jak tłumaczenie mechanizmów na konkretnych przykładach. Według mnie zazwyczaj jest to najlepsze podejście – w końcu na co dzień pracujemy z kodem, więc czemu i na nim by się nie uczyć?

Przygotowałem dla Ciebie projekt zawierający 12 klas, chciałem, aby poziom skomplikowania zależności był trochę większy. Chciałem, aby więcej klas ze sobą „rozmawiało”, żeby w aplikacji się coś działo. Pomimo tego, że sama logika jest bardzo prosta.

Na początek polecam Ci sklonować to repozytorium, otworzyć projekt w swoim ulubionym IDE, a na koniec się przez niego przeklikać. Poznaj jak ułożone są pakiety oraz klasy. Pozwoli Ci to łatwiej zrozumieć to, co chcę Ci za chwilę przekazać.

Przegląd

Na szybko opowiem teraz co się dzieje w projekcie.

Mamy paczkę camel, która zbiera nam wszystkie klasy powiązane z domeną wielbłąda. Wszystkie klasy są zebrane w CamelFacade – przykrywa ona wszystkie niepotrzebne klasy, a wystawia na świat (na zewnątrz paczki) tylko to co chcemy udostępnić.

W CamelFacade znajdziemy dwa serwisy: CamelService oraz GuardianService. Oba te serwisy zawierają w sobie klasy DAO (Data Access Object) oraz walidatory.

Klasy DAO zapisują obiekty do HashMapy, zaś walidatory sprawdzają proste warunki, m.in:

  • czy wielbłąd o danym imieniu już istnieje,
  • czy opiekun o danym imieniu i nazwisku już istnieje,
  • czy opiekun o danym emailu już istnieje.

W całej paczce używam dwóch modeli: Camel oraz Guardian. Pierwsza z nich prezentuje się następująco:

package pl.blog.spring.camel.model;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

public class Camel {
    private Long id;
    private String name;
    private int age;
    private Gender gender;
    private List<Guardian> guardians;


    public Camel(String name, int age, Gender gender, Guardian guardian) {
        this.name = name;
        this.age = age;
        this.gender = gender;
        guardians = new LinkedList<>(Collections.singleton(guardian));
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public List<Guardian> getGuardians() {
        return guardians;
    }

    public int getAge() {
        return age;
    }

    public Gender getGender() {
        return gender;
    }

    @Override
    public String toString() {
        return "Camel{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", gender=" + gender +
                ", guardians=" + guardians +
                '}';
    }
}

Bardzo prosta klasa przechowująca informację o wielbłądzie, oraz klasa Guardian, która nie różni się poziomem skomplikowania od klasy Camel.

package pl.blog.spring.camel.model;

public class Guardian {
    private Long id;
    private String name;
    private String lastName;
    private String email;


    public Guardian(String name, String email, String lastName) {
        this.name = name;
        this.email = email;
        this.lastName = lastName;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public String getLastName() {
        return lastName;
    }

    @Override
    public String toString() {
        return "Guardian{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", lastName='" + lastName + '\'' +
                '}';
    }
}

Strukturę wszystkich zależności (tego jak klasy ze sobą rozmawiają) można obejrzeć na poniższym grafie.

Z jakim problemem się borykamy?

Jeśli całą paczkę camel mamy w jednym palcu i na pierwszy rzut oka nie widać żadnych problemów to warto na chwilę przysiąść i się zastanowić.

Aktualnie mamy 12 klas, z czego aż 7 „rozmawia” z innymi klasami, zaś na świat wystawiamy tylko fasadę.

Fasadę, od której zaczyna się cały graf zależności. Zależności, które musimy dostarczyć podczas tworzenia obiektu typu CamelFacade.

Napiszmy krótkiego maina do naszej paczki, w której zainicjalizujemy naszą fasadę oraz dodamy nowego wielbłąda wraz z opiekunem,

Zerkając na wcześniejszy graf widać, że potrzebujemy stworzyć łącznie 6 obiektów, aby na sam koniec utworzyć naszą upragnioną, długo wyczekiwaną fasadę.

Spróbujmy to zrobić, w końcu jesteśmy na warsztacie, a nie na komercyjnym projekcie. 😉

public class CamelMain {
    public static void main(String[] args) {
        CamelDao camelDao = new CamelDao();
        GuardianDao guardianDao = new GuardianDao();

        CamelValidator camelValidator = new CamelValidator(camelDao);
        GuardianValidator guardianValidator = new GuardianValidator(guardianDao);

        CamelService camelService = new CamelService(camelDao, camelValidator);
        GuardianService guardianService = new GuardianService(guardianDao, guardianValidator);

        CamelFacade facade = new CamelFacade(camelService, guardianService);
    }
}

Nie ma co ukrywać, jest to pracochłonne i nie każdy chciałby robić takie cyrki w swoim projekcie. Szczególnie, że taka fasada mogłaby być używana w wielu miejscach w obrębie całego projektu.

Co prawda jednym z rozwiązań, aby ułatwić sobie pracę oraz sprawić, że nasz kod będzie czytelniejszy może być stworzenie metody, która będzie odpowiedzialna za inicjalizację.

public class CamelMain {
    public static CamelFacade createCamelFacade() {
        CamelDao camelDao = new CamelDao();
        GuardianDao guardianDao = new GuardianDao();

        CamelValidator camelValidator = new CamelValidator(camelDao);
        GuardianValidator guardianValidator = new GuardianValidator(guardianDao);

        CamelService camelService = new CamelService(camelDao, camelValidator);
        GuardianService guardianService = new GuardianService(guardianDao, guardianValidator);

        return new CamelFacade(camelService, guardianService);
    }
    
    public static void main(String[] args) {
        CamelFacade camelFacade = createCamelFacade();
    }
}

Muszę przyznać, że nie jest to takie całkiem głupie – w tym przypadku w jednym miejscu trzymamy logikę budowania całej fasady i dalej używamy jej w takim miejscu jakim chcemy.

Choć to podejście też ma swoje wady, w następnych krokach pokażę Ci jak można to zrobić lepiej.

Context

Pierwszą wadą jaką zauważam w tym rozwiązaniu jest to, że przy każdym tworzeniu obiektu CamelFacade tworzymy wiele innych obiektów. W naszym przypadku owe obiekty mogłyby być Singletonami, w końcu nie przechowują w sobie żadnego stanu.

Moglibyśmy obejść ten problem implementując wzorzec Singleton w każdej z używanych klas, jednak my zrobimy to inaczej. Jedna dedykowana klasa będzie zajmowała się zarządzaniem obiektu, nazwiemy ją CDIManager, od Context Dependency Injection.

Wspomniana klasa ma za zadanie zarządzać obiektami w naszej aplikacji, więc dobrze by było, aby przechowywała w sobie zbiór zarządzanych obiektów. My wykorzystamy do tego mapę:

public class CDIManager {
    private static final Map<Class, Object> objects = new HashMap<>();
}

Napiszmy do tego metodę publiczną, która będzie zwracać instancję obiektu na podstawie podanego typu.

Kluczem w mapie jest Class, używając typów będziemy mogli tam wrzucać i wyciągać stąd odpowiednie instancje obiektów

    public static <T> T get(Class<T> name) {
        return Optional.ofNullable(objects.get(name))
                .map(name::cast)
                .orElseThrow(() -> new IllegalArgumentException("Cannot find implementation for: " + name + " class."));
    }

Chwila wyjaśnienia powyższego kodu. Na początek z mapy wyciągamy na podstawie klucza instację typu Object, następnie musimy ją zrzutować do konkretnego typu – metoda jest parametryzowa T, więc właśnie taki typ chcemy ostatatecznie zwrócić. W przypadku, gdy obiekt konkretnego typu nie istnieje w naszym kontekście to rzucamy wyjątkiem IllegalArgumentException.

Warto też podkreślić, że metoda cast może rzucić nam wyjątkiem ClassCastException w przypadku, gdy w mapie pod kluczem jednego typu znajdzie się obiekt innego typu. W takim przypadku Java nie będzie w stanie dla nas zrzutować na konkretny typ.

Skoro mamy już mechanizm wyciągania instancji to czas uzupełnić naszą mapę – wrzucimy tam oczywiście nasze zależności, z których chcemy korzystać w aplikacji. Wykorzystam do tego statyczny inicjalizator.

    static {
        objects.put(GuardianDao.class, new GuardianDao());
        objects.put(CamelDao.class, new CamelDao());
        objects.put(GuardianValidator.class, new GuardianValidator(get(GuardianDao.class)));
        objects.put(CamelValidator.class, new CamelValidator(get(CamelDao.class)));
        objects.put(GuardianService.class, new GuardianService(get(GuardianDao.class), get(GuardianValidator.class)));
        objects.put(CamelService.class, new CamelService(get(CamelDao.class), get(CamelValidator.class)));
        objects.put(CamelFacade.class, new CamelFacade(get(CamelService.class), get(GuardianService.class)));
    }

Dzięki napisaniu takiego fragmentu kodu w naszej mapie pod odpowiednim typem znajdują się odpowiednie zależności. Mając już gotową klasę, która prezentuje się tak:

package pl.blog.spring;

import pl.blog.spring.camel.CamelFacade;
import pl.blog.spring.camel.dao.CamelDao;
import pl.blog.spring.camel.dao.GuardianDao;
import pl.blog.spring.camel.service.CamelService;
import pl.blog.spring.camel.service.GuardianService;
import pl.blog.spring.camel.validation.CamelValidator;
import pl.blog.spring.camel.validation.GuardianValidator;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class CDIManager {
    private static final Map<Class, Object> objects = new HashMap<>();

    static {
        objects.put(GuardianDao.class, new GuardianDao());
        objects.put(CamelDao.class, new CamelDao());
        objects.put(GuardianValidator.class, new GuardianValidator(get(GuardianDao.class)));
        objects.put(CamelValidator.class, new CamelValidator(get(CamelDao.class)));
        objects.put(GuardianService.class, new GuardianService(get(GuardianDao.class), get(GuardianValidator.class)));
        objects.put(CamelService.class, new CamelService(get(CamelDao.class), get(CamelValidator.class)));
        objects.put(CamelFacade.class, new CamelFacade(get(CamelService.class), get(GuardianService.class)));
    }

    public static <T> T get(Class<T> name) {
        return Optional.ofNullable(objects.get(name))
                .map(name::cast)
                .orElseThrow(() -> new IllegalArgumentException("Cannot find implementation for: " + name + " class."));
    }
}

Wykorzystajmy ją w kolejnym mainie, gdzie będziemy chcieli użyć CamelFacade.

package pl.blog.spring;

import pl.blog.spring.camel.CamelFacade;
import pl.blog.spring.camel.model.Camel;
import pl.blog.spring.camel.model.Gender;
import pl.blog.spring.camel.model.Guardian;

public class Main {

    public static void main(String[] args) {
        CamelFacade camelFacade = CDIManager.get(CamelFacade.class);

        Camel camel = camelFacade.createCamel(new Guardian("Jan", "jan@mail.com", "admin"), "Mela", 15, Gender.FEMALE);

        System.out.println(camel);
    }
}

Tym razem sprawa jest jeszcze prostsza, korzystamy z metody get oraz podajemy jakiego typu obiekt nas interesuje.

W przypadku, gdy instancja interesującego nas obiektu nie istnieje np.:

Guardian guardian = CDIManager.get(Guardian.class);

Zostanie rzucony wyjątek IllegalArgumentException.

Exception in thread "main" java.lang.IllegalArgumentException: Cannot find implementation for: class pl.blog.spring.camel.model.Guardian class.
	at pl.blog.spring.CDIManager.lambda$get$0(CDIManager.java:31)
	at java.util.Optional.orElseThrow(Optional.java:290)
	at pl.blog.spring.CDIManager.get(CDIManager.java:31)
	at pl.blog.spring.Main.main(Main.java:17)

I w tym momencie od razu wiemy, że nasz kontekst nie zawiera takiej klasy.

Powyższe rozwiązanie zwraca nam tylko Singletony, w przypadku gdybyśmy chcieli za każdym razem otrzymywać nowy obiekt, możemy posłużyć się refleksjami.

   public static <T> T getFreshInstance(Class<T> name) {
        try {
            Object o = name.newInstance();
            return name.cast(o);
        } catch (InstantiationException | IllegalAccessException e) {
            throw new IllegalArgumentException(e.getMessage(), e);
        }
    }

Jednak powyższa metoda zadziała tylko w przypadku, gdy podany typ nie jest interfejsem lub klasą abstrakcyjną (można utworzyć instancję) oraz zawiera bezargumentowy konstruktor. W przeciwnym zostanie rzucony wyjątek.

Takie rozwiązanie jest dużo bardziej elastyczne, jednak wymaga dopisania jeszcze sporej ilości kodu, który by ogarniał stworzenie konstruktora i wstrzyknięcie do klasy wszystkich potrzebnych zależności.

W ramach naszego warsztatu nie będziemy się tym zajmować, ponieważ chodzi tylko o koncepcję działania całego kontekstu.

Utrzymywanie metod tworzących obiekty

Kolejną wadą tego rozwiązania jest to, że z każdą zmianą pól w klasie, wraz z każdą nowo tworzoną klasą musimy dodawać ją recznie do kontekstu.

Za każdym razem musimy pilnować tego, żeby nasze zależności znalazły się w kontekście, aby móc z nich później korzystać.

O ile na początku (np. w naszym małym projekcie) nie stanowi to dla nas problemu to w przypadku, gdy projekt się rozrasta i liczba zależności wzrasta, fajnie by było mieć prostszy mechanizm wrzucania zależności do kontekstu i automatycznego ich tworzenia.

Owy problem moglibyśmy rozwiązać na dwa sposoby:

  • użycie pliku, gdzie definiowalibyśmy zależności,
  • oznaczanie klas adnotacjami, które byłyby skanowane w czasie annotation processingu

Powyższe rozwiązania ułatwiłyby nam sprawę, wystarczyłoby oznaczyć klasę np. jako @Dependency, a cały kontekst zająłby się już poprawnym utworzeniem zależności.

Mechanizm na pewno bardzo ciekawy i przydatny, jednak trochę skomplikowany. Sam temat np. annotation processingu nie jest najprostszy, a do tego dochodzą kwestie refleksji, przy użyciu których moglibyśmy tworzyć dynamicznie obiekty wraz ze wszystkimi jej zależnościami.

Moglibyśmy zmarnować X czasu i zaimplementować działające rozwiązanie, ale pytanie – po co? Po co wymyślać koło na nowo?

Wspomnianym kołem w naszym przypadku jest właśnie kontener Springa, który całą powyższą magię robi za nas. Wszystko co Ci pokazałem powyżej: tworzenie obiektów, zarządzanie nimi oraz wstrzykiwanie, ogarnia właśnie on.

Oczywiście przy okazji robi jeszcze wiele innych rzeczy m.in.:

  • możliwość konfiguracji jaka dokładnie implementacja interfejsu ma być używana;
  • wykrywanie circular dependency czyli sytuacji, gdy dwie klasy nazwajem zawierają się w sobie.

Jednak nie jest to na razie dla Ciebie istotne. Zajmijmy się typowymi podstawami.

Migracja projektu

Skoro już wiemy, że całe zarządzanie zależnościami możemy oddać w ręcę Springa to czemu by tego nie zrobić?

Wykorzystajmy do tego poprzednio utworzony projekt i wrzućmy do niego całą paczkę camel z aktualnego projektu. Bez klas main oraz CDIManagera.

Drzewo naszego projektu powinno wyglądać teraz mniej więcej tak:

Drzewo projektu Spring Boot

W tym projekcie nie mamy już własnego kontekstu CDIManager, musimy rozpocząć migrację naszych zależności na Spring Context. Zanim jednak to zrobimy, kilka słów wstępu.

W Springu wyróżniamy takie pojęcie jak bean, bean jest niczym innym jak obiektem całkowicie zarządzanym przez kontekst Springa. To on odpowiada za stworzenie instancji i wstrzykiwanie jej w odpowiednie miejsce. Czyli możemy go utożsamiać z obiektami, które ręcznie wrzucaliśmy do własnego kontekstu (mapy) w tym miejscu:

    static {
        objects.put(GuardianDao.class, new GuardianDao());
        objects.put(CamelDao.class, new CamelDao());
        objects.put(GuardianValidator.class, new GuardianValidator(get(GuardianDao.class)));
        objects.put(CamelValidator.class, new CamelValidator(get(CamelDao.class)));
        objects.put(GuardianService.class, new GuardianService(get(GuardianDao.class), get(GuardianValidator.class)));
        objects.put(CamelService.class, new CamelService(get(CamelDao.class), get(CamelValidator.class)));
        objects.put(CamelFacade.class, new CamelFacade(get(CamelService.class), get(GuardianService.class)));
    }

Właśnie te tworzone obiekty możemy nazwać beanami.

Kolejnym istotnym pojęciem jest Spring context jest modułem, który odpowiada za zarządzanie tymi wszystkimi beanami. W naszym przypadku to klasa CDIManager pełni taką rolę. Teraz tą funkcję przejmuje springowy kontener.

Jak stworzyć bean?

Tworzenie beanów jest bardzo proste i możemy to zrobić na dwa sposoby:

  • poprzez plik xml (podejście stare, rzadko już stosowane),
  • poprzez oznaczenie klasy adnotacją @Component

My zastosujemy nowsze, częściej używane podejście czyli adnotację @Component.

Migracja na korzystanie z beanów Springowych jest, więc bardzo prosta – wystarczy wszystkie nasze zależności oznaczyć tą adnotacją i trafią one automatycznie do kontekstu.

@Component
public class CamelDao {
...

@Component
public class GuardianDao {
...

@Component
public class CamelService {
...

@Component
public class GuardianService {
...

@Component
public class CamelValidator {
...

@Component
public class GuardianValidator {

@Component
public class CamelFacade {
....

Nasze beany trafiły już do kontekstu, ostatnim krokiem jaki musimy poczynić to wskazanie Springowi miejsc, w których chcemy wstrzyknąć nasze zależności. Chodzi tutaj o klasy, w których to chcemy korzystać z wcześniej utworzonych zależności.

Wstrzykiwanie zależności

Wstrzykiwanie beanów jest tak samo proste jak ich tworzenie, wykorzystujemy do tego adnotację @Autowired.

Możemy jej użyć w trzech miejscach:

  • na polu w klasie,
  • metodzie (setter),
  • konstruktorze.

Najbardziej popularnym i wskazanym podejściem jest wstrzykiwanie poprzez konstruktor. Dlaczego?

  • ułatwia testowanie,
  • umożliwia zadeklarowanie pól jako final,
  • zabezpiecza nas przez NullPointerException.

Skoro wiesz już teoretycznie jak wstrzykiwać beany, to zróbmy to w naszym projekcie – dodajmy adnotację @Autowired nad konstruktorami, w których to potrzebujemy wstrzyknąć zależności.

    @Autowired
    public CamelService(CamelDao camelDao, CamelValidator camelValidator) {
        this.camelDao = camelDao;
        this.camelValidator = camelValidator;
    }

    @Autowired
    public GuardianService(GuardianDao guardianDao, GuardianValidator guardianValidator) {
        this.guardianDao = guardianDao;
        this.guardianValidator = guardianValidator;
    }

    @Autowired
    public CamelValidator(CamelDao camelDao) {
        this.camelDao = camelDao;
    }

    @Autowired
    public GuardianValidator(GuardianDao guardianDao) {
        this.guardianDao = guardianDao;
    }

    @Autowired
    public CamelFacade(CamelService camelService, GuardianService guardianService) {
        this.camelService = camelService;
        this.guardianService = guardianService;
    }

Każdy konstruktor został oznaczony, projekt powinien uruchomić się bez problemu. Jednak jeszcze kolejna kwestia – ostatecznie chcemy skorzystać z zależności CamelFacade, aby stworzyć wielbłąda wraz z opiekunem.

Run, Spring, Run!

Aby mieć możliwość wykonania kodu po uruchomieniu serwera aplikacji, wystarczy, że klasa CamelApplication będzie implementowała interfejs CommandLineRunner. Kod napisany w metodzie run zostanie uruchomiony po wstaniu całej aplikacji.

@SpringBootApplication
public class CamelApplication implements CommandLineRunner {

	public static void main(String[] args) {
		SpringApplication.run(CamelApplication.class, args);
	}

	@Override
	public void run(String... args) throws Exception {
	     // code here
	}
}

Następny krok to wstrzyknięcie CamelFacade i możemy zrobić to na dwa sposoby:

  • przy użyciu adnotacji @Autowired,
  • skorzystać z BeanFactory

My skorzystamy z drugiego podejścia, abyś zobaczył jak Spring ogarnia sobie to niżej – gdzie znajdują się te wszystkie nasze Beany.

W tym celu skorzystamy dokładniej z ApplicationContext czyli subinterfejsu BeanFactory. Wstrzyknijmy go sobie standardowo przez konstruktor do klasy CamelApplication.

@SpringBootApplication
public class CamelApplication implements CommandLineRunner {
	private final ApplicationContext applicationContext;

	@Autowired
	public CamelApplication(ApplicationContext applicationContext) {
		this.applicationContext = applicationContext;
	}

	public static void main(String[] args) {
		SpringApplication.run(CamelApplication.class, args);
	}

	@Override
	public void run(String... args) throws Exception {
	}
}

Napiszmy teraz implementację metody run. Na początek z kontekstu aplikacji musimy wyciągnąć beana CamelFacade. Robimy to w bardzo prosty sposób, korzystając z metody getBean:

@Override
	public void run(String... args) throws Exception {
		CamelFacade camelFacade = applicationContext.getBean(CamelFacade.class);
	}

Zauważ, że jest to bardzo podobne do tego jak my to zaimplementowaliśmy na samym początku:

 CamelFacade camelFacade = CDIManager.get(CamelFacade.class);

Reszta kodu zostaję już tak naprawdę bez zmian:

	@Override
	public void run(String... args) throws Exception {
		CamelFacade camelFacade = applicationContext.getBean(CamelFacade.class);
		Camel camel = camelFacade.createCamel(new Guardian("Jan", "jan@mail.com", "admin"), "Mela", 15, Gender.FEMALE);

		System.out.println(camel);
	}

Po uruchomieniu aplikacji powinieneś otrzymać logi podobne do tych:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.6.RELEASE)

2019-08-03 10:56:02.876  INFO 1384 --- [           main] pl.blog.spring.camel.CamelApplication    : Starting CamelApplication on L1429 with PID 1384 (C:\Users\klimekk\Desktop\spring-context\spring\target\classes started by klimekk in C:\Users\klimekk\Desktop\spring-context\spring)
2019-08-03 10:56:02.879  INFO 1384 --- [           main] pl.blog.spring.camel.CamelApplication    : No active profile set, falling back to default profiles: default
2019-08-03 10:56:03.346  INFO 1384 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data repositories in DEFAULT mode.
2019-08-03 10:56:03.365  INFO 1384 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 14ms. Found 0 repository interfaces.
2019-08-03 10:56:03.569  INFO 1384 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$cb80142c] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2019-08-03 10:56:03.737  INFO 1384 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8090 (http)
2019-08-03 10:56:03.754  INFO 1384 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2019-08-03 10:56:03.755  INFO 1384 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.21]
2019-08-03 10:56:03.857  INFO 1384 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2019-08-03 10:56:03.857  INFO 1384 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 948 ms
2019-08-03 10:56:03.945  INFO 1384 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2019-08-03 10:56:04.010  INFO 1384 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2019-08-03 10:56:04.048  INFO 1384 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
	name: default
	...]
2019-08-03 10:56:04.147  INFO 1384 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {5.3.10.Final}
2019-08-03 10:56:04.148  INFO 1384 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2019-08-03 10:56:04.235  INFO 1384 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.0.4.Final}
2019-08-03 10:56:04.325  INFO 1384 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2019-08-03 10:56:04.459  INFO 1384 --- [           main] o.h.t.schema.internal.SchemaCreatorImpl  : HHH000476: Executing import script 'org.hibernate.tool.schema.internal.exec.ScriptSourceInputNonExistentImpl@6812fa3a'
2019-08-03 10:56:04.462  INFO 1384 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2019-08-03 10:56:04.631  INFO 1384 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2019-08-03 10:56:04.658  WARN 1384 --- [           main] aWebConfiguration$JpaWebMvcConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2019-08-03 10:56:04.710  WARN 1384 --- [           main] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration)
2019-08-03 10:56:04.805  INFO 1384 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8090 (http) with context path ''
2019-08-03 10:56:04.807  INFO 1384 --- [           main] pl.blog.spring.camel.CamelApplication    : Started CamelApplication in 2.18 seconds (JVM running for 2.726)
Camel{id=0, name='Mela', age=15, gender=FEMALE, guardians=[Guardian{id=0, name='Jan', email='jan@mail.com', lastName='admin'}]}

Na samym końcu powinieneś dostrzec opis wielbłąda.

Na koniec jeszcze zaznaczę, że metoda applicationContext.getBean jest rzadko stosowana, zazwyczaj będziesz używał adnotacji @Autowired. Chciałem Ci tylko ją pokazać, abyś upewnił się, że kontekst Springa działa podobnie jak ten, który sami napisaliśmy. Oczywiście ze szczyptą magii (czyt. refleksji). 😉

Inversion of Control

Wyjdźmy z warsztatu i na chwilę zatrzymajmy się przy pojęciu Inversion of Control. IOC oznacza odwrócenie sterowania, lecz na czym to polega?

Wzorzec projektowy IOC mówi, że sterowanie jest zadaniem frameworka z jakiego korzystamy – nie musimy robić tego ręcznie.

IOC jest często utożsamiany z dependency injection, jednak warto zaznaczyć, że to DI jest tylko jednym z przypadków, gdy sterowanie zostaje przeniesione na framework.

W naszym przypadku frameworkiem jest Spring, a przenosimy na niego odpowiedzialność sterowania zależnościami – czyli to Spring Context tworzy dla nas zależności i wstrzykuje je w odpowiednie miejsce.

Nie musimy sami martwić się o zarządzanie obiektami, robi to dla nas cały kontekst Springa.

Podsumowanie

Gdybyś z tego artykułu miał zapamiętać tylko dwie rzeczy to chciałbym, aby to było:

  • Bean to obiekt, który jest całkowicie zarządzany przez Springa – lubię określenie Jarka Ratajskiego: „Beany to takie specjalne klasy […]”;
  • Spring Context można utożsamiać z mapą obiektów – Spring na podstawie typu je przechowuje i wstrzykuje w momencie kiedy tego potrzebujemy.

Abyś jak najwięcej mógł wyciągnąć z tego artykułu przygotowałem do wykonanie dla Ciebie pracę domową. Twoim zadaniem jest dopisanie kawałka funkcjonalności do projektu, który został stworzony w tym wpisie.

W paczce model, znajdziesz klasę Inspector, odpowiedzialną za reprezentację osoby, która kontroluje wielbłądy.

public class Inspector {
    private Long id;
    private String name;
    private List<Camel> camelsToInspect;
}

Twoim zadaniem jest:

  • stworzenie klasy InspectorDAO;
  • stworzenia walidatora, który będzie sprawdzał czy tworzony inspektor ma listę wielbłądów do skontrolowania – jeśli nie ma, niech zostanie rzucony wyjątek;
  • stworzenie serwisu, który będzie walidował i tworzył inspektorów;
  • dodanie metody do fasady, umożliwiającej tworzenie inspektora wraz z listą wielbłądów do sprawdzenia.

Rozwiązanie tego zadanie, znajdziesz w tym repozytorium na branchu: inspector-solution.

I to już tyle, w tym wpisie pokazałem Ci najważniejszą część Springa, bez której nie byłbyś w stanie ruszyć dalej.

Jest to dopiero początek drogi tworzenia aplikacji, w następnych artykułach będzie już bardziej ciekawie. W końcu nadejdzie czas na tworzenie kontrolerów, połączenie z bazą danych oraz stworzenie pierwszego widoku HTML! 🙂