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.

By być na bieżąco i mieć realny wpływ na tematykę tworzonych przeze mnie artykułów zapraszam do dołączenia do mojego newslettera.

Dodaj komentarz

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