REST API + Thymelaf - Architektura aplikacji webowej

0

Zastanawiam się nad sensem tworzenia REST-API w Springu. Mam gotowe webowe API i chciałbym na podstawie tego API zrobić widok w HTML-u z wykorzystaniem Thymelaf ale tak by nie zmieniać klasy @RestController na razie jest okej są metody, wykonują się operacje CRUD na bazie ale obsługuje to z poziomu Postman-a. Sądziłem, że jak mam takie API zrobienie widoku w HTML-u będzie łatwe a okazuje się, że coś mi umknęło. Logika całej aplikacji mi się wali, wychodzi mi że muszę popsuć kontroler tak by zwracał widoki (ModelAndView) lub napisać kod w technologii JS-a by łykał JSON-y i na tej podstawie budował HTML-a. Czytałem o RestTemplate ale brakuje mi informacji jak przy użyciu tej klasy wykorzystać Thymeleaf i czy w ogóle się da.

Jak zacznę dłubać w @RestController to klasa stanie nieużyteczna dla kogoś będzie chciał napisać aplikacje kliencką. Nie wiem czy myślę dobrze, mam mętlik. Proszę o pomoc w wyjaśnieniu kwestii.

0

Z opisu wydaje mi się kolego, że masz w tym kontrolerze jakąś logikę, którą chcesz reużyć. Dopisanie RestControllera nie powinno być problemem, jeśli logikę miałbyś schowana w serwisie. Druga sprawa - RestTemplate nie ma tutaj nic do rzeczy, totalnie mieszasz pojęcia i warstwy. Sprawa trzecia - jak chcesz konsumować restowe API JS-em, to weź sobie jakiegoś Angulara albo Reacta. Thymeleafa możesz wtedy użyć tylko po to, żeby zaserwować tego JS-a ze Springa, ale to i tak niepotrzebne, bo możesz zaserwować po prostu statica.

0

Faktycznie, mętlik w głowie.

Masz tu przykład dwóch kontrollerów - @Controller i @RestController

@Controller
public class Controller1 {
    
    @GetMapping("/home")
    private String homePage() {
        return "home";
    }
}
@RestController
public class Controller2 {
    
    @GetMapping("/title")
    private String getTitle() {
        return "hello";
    }
}

Obydwa wystawiają po jednym endpoincie, które możesz przetestować sobie w postmanie:
GET /home - zwróci zawartość pliku home.html
GET /title - zwróci dosłownie String "hello"

I taka jest różnica między @Controller i @RestController. Ciebie interesuje pierwszy rodzaj jeśli chcesz korzystać z thymeleafa. Chcesz zwracać pliki html, a nie wystawiać REST-owe endpointy. Oczywiście chcesz jakąś zmienność wprowadzać, a nie zwracać statyczne pliki .html, więc możesz dodawać obiekty do modelu:

@Controller
public class Controller1 {
    
    @GetMapping("/home")
    private String homePage(Model model) {
        model.addAttribute("title", "hello"); // Doda stringa "hello" dostępnego pod nazwą title
        return "home";
    }
}

A potem w pliku home.html korzystać z thymeleafa np. tak:

<html>
    ...
    <body>
        <div th:if="${title.length() > 0}">${title}</div>
    </body>
</html>

Oczywiście, do wszelkich operacji CRUDowych będziesz musiał zdefiniować logikę w JavaScripcie w tych plikach html, która będzie uderzać do wystawionego przez Ciebie RESTa.

RestTemplate się do niczego nie przyda w tym przypadku. Używasz go do konsumowania zewnętrznego REST API.

0

Jest sobie takie API, przedstawiam fragment, co zwraca mi fajnie JSON-a, dane zapisuje na poziomie bazy.

@RestController
public class OwnerController extends RestExceptionHandler {
    private final OwnerRepository ownerRepository;

    public OwnerController(OwnerRepository ownerRepository) {
        this.ownerRepository = ownerRepository;
    }
    @GetMapping()
    public Iterable<Owner> getAllOwners() {
        return ownerRepository.findAll();
    }
    @GetMapping("/{id}")
    public Owner getOwner(@PathVariable Integer id) {
        Owner owner = ownerRepository.findOwnerById(id);
        if (owner == null) throw new EntityNotFoundException(id);
        return owner;
    }
@PostMapping()
    @ResponseStatus(HttpStatus.CREATED)
    public void addOwner(@RequestBody Owner body) {
        ownerRepository.save(body);
    }
}

Nie istnieją warstwy pośrednie, chciałbym jakoś skonsumować to API tak aby użytkownik dostał widoki HTML-u. Nie chce wprowadzać zmian w tej konkretnej klasie, bo wtedy to API będzie nie do użytku dla innych klientów. Chciałbym móc to zrobić dla Thymeleaf.

Na razie wyszło tak, co mnie nie zadowala:

@RestController
public class OwnerController extends RestExceptionHandler {
    private final OwnerRepository ownerRepository;

    public OwnerController(OwnerRepository ownerRepository) {
        this.ownerRepository = ownerRepository;
    }
    @GetMapping()
    public ModelAndView getAllOwners() {
        ModelAndView ownersListView = new ModelAndView("owners/owners_list");
        ownersListView.addObject("owners", ownerRepository.findAll());
        return ownersListView;
    }
    @GetMapping("/{id}")
    public ModelAndView getOwner(@PathVariable Integer id) {
        ModelAndView ownerView = new ModelAndView("owners/edit_owner_data");
        Owner owner = ownerRepository.findOwnerById(id);
        if (owner == null) throw new EntityNotFoundException(id);

        ownerView.addObject("owner", owner);
        return ownerView;
    }
@PostMapping()
    @ResponseStatus(HttpStatus.CREATED)
    public ModelAndView addOwner(Owner body) {
        ownerRepository.save(body);

ModelAndView ownerListView = new ModelAndView("owners/owners_list");
        ownerListView.addObject("owners", repository.findAll());
        return ownerListView;
    }
}

Rozwiązanie oczywiście się sprawdza, ale muszę zmieniać zachowanie kontrolera względem tego co chce mieć w widoku. Sądziłem, że warstwy aplikacji mogą być niezależne.

Obecnie rozważam zrobienie kolejnej warstwy, która konsumuje API i integruje to z widokiem. Dużo jest napisane rzeczy w Internecie ale akurat o architekturze aplikacji REST z widokiem nic nie znalazłem.

1

Nie rozumiem twojego problemu, nie możesz zrobić @RestController(/api) do API i obok niego @RestController(/app) do tych danych w formie widokowej?

2

Kod wyglada bardzo znajomo - nawet jest wersja z Rest API i Angularem :) jeśli chcesz mieć i thymeleafa i Rest API, to powinieneś wprowadzić warstwę serwisowo-aplikacyjną, która po prostu będziesz wolał z obu kontrolerów. Nie rób czegoś takiego, że aplikacja będzie wolała sama siebie po HTTP, żeby reuzyc kod, to jest bez sensu.

0

Zrobiłem klasę, w zasadzie to nadbudowa na kontrolery, ale dodaje widoki zwracając ModelAndView. Inaczej chyba się nie da dla Thymeleaf, inny sposób to napisanie zamiany JSON w HTML przy pomocy JS-owego frameworka.

@Controller("OwnerConsumer")
@RequestMapping("/web")
public class ThymeleafBuilder {

    private final OwnerController ownerController;
    private final PaymentsController paymentsController;

    public ThymeleafBuilder(OwnerController ownerController, PaymentsController paymentsController) {
        this.ownerController = ownerController;
        this.paymentsController = paymentsController;
    }
    /* ------------------------- Metody GET --------------------- */
        @RequestMapping(method = RequestMethod.GET)
        public ModelAndView startWeb() {
            return new ModelAndView("builder-test");
        }
        @RequestMapping(value="/add-owner", method = RequestMethod.GET)
        public ModelAndView redirectToOwnerAddForm() {
            return new ModelAndView("add-owner-form", "owner", new Owner());
        }
        @RequestMapping(value="/add-payment/{id}", method = RequestMethod.GET)
        public ModelAndView redirectToPaymentAddForm(@PathVariable Integer id) {
            ModelAndView view = new ModelAndView("add-payment-form");
            view.addObject("owner", ownerController.getOwner(id));
            view.addObject("payment", ownerController.getOwnerPayments(id));
            return view;
        }

itd...

0
MrMadMatt napisał(a):

Nie rozumiem twojego problemu, nie możesz zrobić @RestController(/api) do API i obok niego @RestController(/app) do tych danych w formie widokowej?

Zależy mi na budowie warstwowej, idea jest taka by nie psuć poprzedniej warstwy celem zrobienia widoku dla webu. Tym sposobem, ktoś będzie mógł użyć tego API, skoro zwraca właściwe dane w formie JSON-a np w aplikacji na Androida.

0

Czy znane Ci jest takie pojęcie jak refactoring? Co to znaczy idea jest taka by nie psuć poprzedniej warstwy celem zrobienia widoku dla webu? Przecież ewidentnie brakuje Ci warstwy serwisów.

0
Charles_Ray napisał(a):

Czy znane Ci jest takie pojęcie jak refactoring? Co to znaczy idea jest taka by nie psuć poprzedniej warstwy celem zrobienia widoku dla webu? Przecież ewidentnie brakuje Ci warstwy serwisów.

No brakuje tego serwisu, adnotacja @Service to warstwa pomiędzy danymi a klasą, która udostępnia wszystko w formie adresów HTTP @RestController. Po co mi ten serwis? Skoro ja chce zrobić widoki, te widoki wyprowadzić z tego serwisu - kurcze muszę pomyśleć, nie lubię nie widzieć sedna sprawy ani istoty problemu. Jak zrobić dwa widoki pod jeden kontroler np. taki dla JS i taka co tworzy Thymeleaf przecie nie mogę zespuć tego kontrolera by zrobić jedno i drugie, jak chce konsumować dane w formie JSON-a nie może mi kontroler zwracać co innego i na odwrót jak chce mieć HTML-a to nie mogę zwracać JSON-a; jedynie co mi się nasuwa kolejna warstwa nie rozumiem jednak po co ten diabelny serwis. Eh, zrobienie prostej aplikacji webowej w Javie to straszna dłubanina, ten język chyba się do tego nie nadaje. Wprowadzam kolejne warstwy abstrakcji by zrobić coś w bardzo banalnego... :(

1
adnotacja @Service to warstwa pomiędzy danymi a klasą, która udostępnia wszystko w formie adresów HTTP @RestController

Istotą zadania jest to, że wspólny kod powinien być wyniesiony do osobnej klasy, którą użyjesz z 2 innych klas. Nie dorabiaj do tego filozofii. To czy na tej klasie będzie @Service czy @Component, sprawa drugorzędna.

przecie nie mogę zespuć tego kontrolera by zrobić jedno i drugie

Zafiksowałeś się na jakieś niepsucie kontrolera, zupełnie nie rozumiem tego toku rozumowania.

jedynie co mi się nasuwa kolejna warstwa nie rozumiem jednak po co ten diabelny serwis.

Zamiast tworzyć kolejną warstwę webową, stwórz po prostu "diabelny serwis". Nie wstrzykuje się kontrolera w kontroler, to jest bez sensu.

Eh, zrobienie prostej aplikacji webowej w Javie to straszna dłubanina, ten język chyba się do tego nie nadaje. 

Bardzo się do tego nadaje, to jest 5 minut roboty.

0

Nie pisze w javie, wygooglałem coś takiego
https://stackoverflow.com/questions/4917329/return-json-or-view-from-spring-mvc-controller
Może można tam jakoś w akcji sprawdzić nagłówek Accepted z Request, jak jest application/json zwracasz json, text/html zwraszasz widok.

0

W takim razie architektura REST jest bez sensu, skoro nie mogę wykorzystać tego interfejsu do stworzenia widoku. Ja zakładam sytuację, że tworzę zupełnie osobną aplikację która konsumuje serwis w chmurze. Inaczej mam np. API WordPress i chce zrobić sobie aplikacje klienta co zbuduje mi nowy widok przy wykorzystaniu Javy, Springa i Thymeleaf. Może ja nie rozumiem po co się używa API. Dziękuję za pomoc ale chyba nadal nie rozwiązałem problemu, nie wiem po co mi RESTowe API 🙂

1

Uściślijmy Chcesz zrobić:

  • jedną aplikację javową, która wystawia dane z bazy za pomocą RESTa
  • drugą aplikację javową, która używa tych danych i na ich podstawie buduje HTMLa

Zgadza się?

Zgadzam się z Tobą. To nie ma sensy. Lepiej żeby to byłą jedna aplikacja. Co oczywiście nie znaczy że nie pracowałem przy takich aplikacjach działających na produkcji. Tylko zamiast RESTa były WebServisy.
REST (a w ogólności cały przez Ciebie wprowadzony tu podział) ma sens jeśli:

  • Ta druga aplikacja jest w JavaScripcie i renderujesz HTMLa w przeglądacie za pomocą Vue lub czegoś podobnego
  • Ta druga aplikacja jest aplikacją mobilną
  • Chcesz niezależnie skalować aplikacje pierwszą i drugą. Wtedy masz magiczne MicroService i inne BuzzWordy
1

Oświeciło mnie, a to dlatego, że wydaje mi się rozumiem co chce @Pangeon. Odrobiłem tez lekcje @jarekr000000 odnośnie wstrzykiwania co najwyżej w konstruktor.
Dalej idzie refleksja (w głowie, nie w Javie), że adnotacje to dodatek do kodu (środowisko serwerowe przekształca na HTTP), ale do k... nędzy nie odbiera nam możliwości normalnego wywołania.

Rozwiązaniem jest POJC (Plain Old Java Code albo Plain Old Java Call). Zmieniam fragmenty kodu z tego watku, aby były bardziej wyraziste. Zauważycie, że stawiam domniemanie, że wspólny pakiet.

@RestController
public class Controller2 {

    @GetMapping("/title")
    /*private*/  List<String> getTitles() {
        return ... 
    }
}

Jego "konsument" w pierwszej wersji

@Controller
public class Controller1 {

    @GetMapping("/home")
    private String homePage(Model model) {
        Controller2  ctrl2 = new Controller2 (...);
        List<String> list = ctrl2.getTitles();
        model.add....();
        return "home";
    }
}

lub wstrzykiwanej

@Controller
public class Controller1 {
    Controller1 (@Inject Controller2  ctr2){
        this.ctrl2 = ctr2;
   }
    @GetMapping("/home")
    private String homePage(Model model) {
        
        List<String> list = ctrl2.getTitles();
        model.add.... ();
        return "home";
    }
}

Może naruszam czyjeś odczucia religijne, ale tak to widzę. Zapraszam do dyskusji.

1
Pangeon napisał(a):

W takim razie architektura REST jest bez sensu, skoro nie mogę wykorzystać tego interfejsu do stworzenia widoku. Ja zakładam sytuację, że tworzę zupełnie osobną aplikację która konsumuje serwis w chmurze. Inaczej mam np. API WordPress i chce zrobić sobie aplikacje klienta co zbuduje mi nowy widok przy wykorzystaniu Javy, Springa i Thymeleaf. Może ja nie rozumiem po co się używa API. Dziękuję za pomoc ale chyba nadal nie rozwiązałem problemu, nie wiem po co mi RESTowe API 🙂

Architektura REST ma sens, ale architektura twojej aplikacji niekoniecznie. Nie jestem ekspertem, ale właśnie w ramach nauki Springa robię to samo co ty. Stworzyłem sobie kawałek aplikacji Spring Rest a następnie chce zaprezentować dane na stronie https://howtodoinjava.com/spring-boot2/resttemplate/spring-restful-client-resttemplate-example/ stronki z Thymeleaf to nie masz potrzeby, żeby korzystać z Rest api, bo masz dostęp do wszystkiego poprzez serwisy i wciskanie trochę na siłę pośredniej warstwy Rest api dla zasady posiadania Rest api mija się z celem, moim zdaniem. Podejrzewam, że "struktura linków" strony webowej jest inna niż "struktura linków" REST API ponieważ mają inne przeznaczenie więc już tutaj mam wątpliwości czy kontroler strony web może być tożsamy z kontrolerem rest. REST API jest jednym ze sposobów przesyłania/modyfikacji danych między systemami. Jak coś udostępnia REST API to jest to to co oferuje - te jsony - api nie służy do tworzenia widoków w Springu.
Czy możesz mieć w aplikacji webowej rest api - jak najbardziej.
Na przykład aplikacja ma wymagania aby funkcjonalności były dostępne poprzez stronę internetową, ale też przez REST API jak alternatywę dostępu do danych.
Na przykład, tworzysz stronkę która w przeglądarce będzie pytać o dane REST API (coś w stylu, że strona się nie przeładowuje ale dane widoki tak, niejako w tle to działa, słyszałem kiedyś słówko Ajax może on ma też z tym coś wspólnego i strona jest wtedy dynamiczna i jak coś klikam to zawsze wtedy akurat coś się zaczyta i przyciski się przesuwają i klikam na coś innego niż chciałem :/).
Natomiast twój (nasz) problem rozważyłem w innym nieco kontekście. Co jak zamiast np. bazy danych dane mam dostępne poprzez REST API? Wtedy moja aplikacja byłaby klientem dla tego api. Wyskoczyło mi w googlach Spring RestTemplate, który wspiera konsumpcję REST API (przynajmniej tak to zrozumiałem)
https://howtodoinjava.com/spring-boot2/resttemplate/spring-restful-client-resttemplate-example/
Czy twoja aplikacja może być klientem dla swojego rest api i wysyłać requesty na localhosta? Chyba tak.
Ja raczej się zdecyduję na oddzielne kontrolery dla weba i rest, które będą sobie korzystały z serwisów i tylko dopasowywały to co zwrócą do tego co chce zaprezentować na stronie.
Te rest api co napisałem nie będę kasował bo może mi się przydadzą jak na przykład będę chciał się nauczyć jak React czy Angular by korzystał z Rest API.

1 użytkowników online, w tym zalogowanych: 0, gości: 1