W ostatnich dwóch częściach mini-serii o architekturze opisałem czym są obiekty domenowe, dto oraz jak wygląda architektura trójwarstwowa/wielowarstwowa.
Warto pokazać kod takiej aplikacji. Użyjemy do tego frameworka Spring, który jest najpopularniejszym frameworkiem w świecie Javy.
W tym wpisie używamy Spring Framework. Jego fundamenty możesz poznać w tym darmowym kursie.
Zacznijmy od utworzenia projektu na https://start.spring.io
Potrzebujemy trzech dependencji – Spring Data JPA (persystencja), H2( (lokalna baza danych), Spring Web (warstwa „prezentacji”)
Warstwa logiki biznesowej
Zaczniemy od funkcjonalności naszego programu, bo w końcu ta jego część zarabia. Nie będziemy się silić w tym przykładzie na oryginalność i wykorzystamy obiekty domenowe z kursu o fundamentach JPA – Reservation i Guest, z ustawionymi adnotacjami persystencji. Nasz program ma służyć do tworzenia rezerwacji hotelowych, więc te dwa obiekty są naszymi głównymi graczami.
package pl.clockworkjava.threetier.guest; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Entity public class Guest { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; private String name; private int age; private String personalIdentificationNumber; public Guest() { } public Guest(String name, int age, String personalId) { this.name = name; this.age = age; this.personalIdentificationNumber = personalId; } public long getId() { return this.id; } public void setAge(int newAge) { this.age = newAge; } public int getAge() { return this.age; } public String getName() { return name; } public String getPersonalIdentificationNumber() { return personalIdentificationNumber; } }
package pl.clockworkjava.threetier.reservation; import pl.clockworkjava.threetier.guest.Guest; import javax.persistence.*; import java.util.List; @Entity public class Reservation { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; @OneToMany private List<Guest> guests; public Reservation(List<Guest> guest) { this.guests = guest; } }
Poza obiektami domenowymi w warstwie logiki biznesowej potrzebujemy jeszcze serwisów. Są to obiekty, które udostępniają operację w danej domenie biznesowej. Podstawowym z nich będzie pobierz wszystkich gości. Teraz w zależności od tego jak zadecydowano w projekcie/zespół zadecyduje, albo zwracamy obiekty domenowe Guest i wychodzą one „poza domenę”, albo już na tym etapie tworzymy DTO i je wystawiamy na świat. Ja wybiorę pierwszą opcję.
package pl.clockworkjava.threetier.guest; import org.springframework.stereotype.Service; import java.util.Collections; import java.util.List; @Service public class GuestService { public List<Guest> getAllGuests() { return Collections.emptyList(); } }
Warto zwrócić uwagę na dwie rzeczy. Po pierwsze adnotacja @Service. Jest to oznaczenie równoważne w funkcjonalności adnotacji @Component, jednak dzięki użyciu @Service osoba czytająca kod od razu wie z jakim typem elementu programu ma do czynienia.
Po drugie zwracamy pustą listę… dlaczego?
Ano dlatego, że za operację z radziny CRUD odpowiada warstwa persystencji, a konkretnie element Repozytorium (dawniej zwany DAO)
Warstwa utrwalania danych
na tej warstwie żyją repozytoria, czyli specjalne obiekty odpowiedzialne za operacje CRUD na danej domenie, oraz za wyszukiwanie po kluczach.
W Springu repozytoria podobnie jak serwisy oznaczamy jako @Repository i również ma to głównie znaczenie dla programisty czytającego kod.
package pl.clockworkjava.threetier.guest; import org.springframework.stereotype.Repository; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import java.util.List; @Repository public class GuestRepository { @PersistenceContext EntityManager entityManager; public List<Guest> getAllGuests() { return entityManager .createQuery("Select guest from Guest guest", Guest.class) .getResultList(); } }
Repozytorium odpowiada więc za wszelkie operacje na Entity Managerze, lub bezpośrednio na bazie danych.
Tak więc skoro w repozytorium zaimplementowaliśmy pobieranie wszystkich gości, to możemy teraz użyć go w serwisie.
package pl.clockworkjava.threetier.guest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class GuestService { @Autowired GuestRepository repository; public List<Guest> getAllGuests() { return repository.getAllGuests(); } }
Warstwa prezentacji
Nadszedł czas na finalną warstwę, czyli prezentacji danych. Prezentacja ta może mieć najróżniejszą postać. My wystawimy na świat REST API.
Dla przykładu wystawimy jedną metodę
package pl.clockworkjava.threetier.guest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController public class GuestController { @Autowired GuestService service; @GetMapping("guest") public List<Guest> getAllGuests() { return service.getAllGuests(); } }
Wszsytko pięknie poza jedną rzeczą – obiekt domenowy Guest posiada wrażliwe dane. W obecnych czasach wszelkie dane osobowe są wrażliwe, ale konkretnie chodzi nam o PESEL.
Możemy więc go wyciąć, najprościej jest zdefiniować DTO, tam dodać tylko niezbędne dane i je wysyłać „w świat”.
package pl.clockworkjava.threetier.guest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; @Service public class GuestService { @Autowired GuestRepository repository; public List<GuestReadDTO> getAllGuests() { return repository.getAllGuests() .stream() .map(guest -> new GuestReadDTO(guest.getAge(), guest.getName(), guest.getId())) .collect(Collectors.toList()); } }
package pl.clockworkjava.threetier.guest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController public class GuestController { @Autowired GuestService service; @GetMapping("guest") public List<GuestReadDTO> getAllGuests() { return service.getAllGuests(); } }
Po uruchomieniu programu i odwiedzając z przeglądarki adres http://localhost:8080/guest powinniśmy otrzymać w odpowiedzi pustą tablicę. W naszej bazie brakuję jakichkolwiek danych. Stwórzmy więc komponent odpowiedzialny za inicjalizacje bazy danymi.
Po pierwsze utwórzmy metodę odpowiedzialną za tworzenie nowych obiektów domenowych w repozytorium
package pl.clockworkjava.threetier.guest; import org.springframework.stereotype.Repository; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.transaction.Transactional; import java.util.List; @Repository public class GuestRepository { @PersistenceContext EntityManager entityManager; public List<Guest> getAllGuests() { return entityManager .createQuery("Select guest from Guest guest", Guest.class) .getResultList(); } @Transactional public void create(String name, int age, String personalId) { Guest newOne = new Guest(name, age, personalId); entityManager.persist(newOne); } }
Teraz utwórzmy komponent implementujący interfejs CommandLineInterface. Metoda run jest uruchamiana, gdy tylko kontekst Spring zostanie stworzony.
package pl.clockworkjava.threetier; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import pl.clockworkjava.threetier.guest.GuestRepository; @Component public class DBInit implements CommandLineRunner { @Autowired private GuestRepository guestRepository; @Override public void run(String... args) throws Exception { this.guestRepository.create("Paweł", 22, "1111111"); } }
Po ponownym uruchomieniu aplikacji i odwiedzeniu adresu localhost:8080/guest powinniśmy dostać informacje o gościu o imieniu Paweł, wieku 22 i id sytemowym 1. Nie ma już informacji o PESEL.
Podsumowanie
Stereotypowe komponenty Springowe (serwis, repozytorium, kontroler) doskonale wpisują się w trójwarstwowy podział aplikacji, który pozwala na odseparowanie od siebie obowiązków poszczególnych warstw. Oczywiście nie jest to dzieło przypadku.
Kod aplikacji znajdziesz w publicznym repozytorium GitHub.
P.S. Jeśli chcesz wiedzieć jak stworzyć aplikację full-stack (ReactJS+Spring Framework) zapraszam do rzucenia okiem na te warsztaty programistyczne (2h kurs)