Architektura warstwowa w praktyce w Spring Framework

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)

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *