Skip to content

3. Akcje: odpowiedź

Rozważmy architekturę aplikacji Spring MVC:

W tym rozdziale przyjrzymy się procesowi, w ramach którego żądanie [1] trafia do kontrolera i akcji [2a], które je przetworzą – mechanizm ten nazywamy routingiem. Przedstawiamy również różne odpowiedzi [3], jakie akcja może zwrócić do przeglądarki. Może to być coś innego niż widok V [4b].

3.1. Nowy projekt

Tworzymy nowy projekt Spring MVC:

  • w [1-2] tworzymy nowy projekt oparty na Spring Boot;
  • w [3] – nazwa projektu Maven;
  • w [4] – grupa Maven, w której zostanie umieszczony wynik kompilacji projektu;
  • w [5] – nazwa nadana wynikowi kompilacji;
  • w [6] – opis projektu;
  • w [7] – pakiet, w którym zostanie umieszczona klasa wykonywalna projektu;
  • w [8] – charakter projektu. Jest to projekt internetowy z widokami Thymeleaf. Widoczne są tutaj wszystkie gotowe do użycia zależności Maven oferowane przez projekt Spring Boot;
  • w polu [9] określa się, że produkt powstały w wyniku kompilacji Maven zostanie spakowany do archiwum jar, a nie war. Projekt będzie wówczas korzystał z wbudowanego serwera Tomcat, który znajdzie się wśród jego zależności;
  • w pliku [10] przechodzimy do kolejnego etapu kreatora;
  • w [11] należy wskazać katalog projektu;
  • w [12] – wygenerowany projekt;
  • w [14-15] zmieniamy nazwę pakietu na [istia.st.springmvc];
  • na [16], nowa nazwa pakietu;
  • w [17] – nowy projekt;

Teraz tworzymy nową klasę;

  • w [1-3] tworzymy nową klasę;
  • w [5] nadajemy jej nazwę, a w [4] określamy jej pakiet;
  • w [6] nowy projekt;

Na razie klasa wygląda następująco:


package istia.st.springmvc;

public class ActionsController {

}

Modyfikujemy ten kod w następujący sposób:


package istia.st.springmvc;

import org.springframework.web.bind.annotation.RestController;

@RestController
public class ActionsController {

}
  • wiersz 6: adnotacja [@RestController] wskazuje na dwie rzeczy:
    • że klasa [ActionsController], opatrzona tą adnotacją, jest kontrolerem Spring MVC, a zatem zawiera akcje przetwarzające URL od klientów;
    • że wynik tych akcji jest wysyłany do klienta;

Inna adnotacja [@Controller], z którą się spotkaliśmy, jest inna: akcje kontrolera opatrzonego tą adnotacją zwracają nazwę widoku, który ma zostać wyświetlony. To właśnie połączenie tego widoku z modelem utworzonym przez akcję dla tego widoku stanowi odpowiedź wysyłaną do klienta.

Zmiana struktury naszego projektu pociąga za sobą zmianę jego konfiguracji:

  

Klasa [Application] zmienia się w następujący sposób:


package istia.st.springmvc.main;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan({"istia.st.springmvc.controllers"})
@EnableAutoConfiguration
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • wiersz 9: adnotacja [ComponentScan] przyjmuje jako parametr tablicę nazw pakietów, w których Spring Boot ma szukać komponentów Spring. W tym przypadku umieszczamy w tej tablicy pakiet [istia.st.springmvc.controllers], aby kontroler oznaczony adnotacją [@RestController] został znaleziony;

Stworzymy różne akcje w kontrolerze, aby zilustrować ich główne cechy. Najpierw skupimy się na różnych typach odpowiedzi, jakie może zwracać akcja w aplikacji bez widoków.

3.2. [/a01, /a02] – Hello world

Nasza pierwsza akcja będzie wyglądała następująco:


@RestController
public class ActionsController {
    // ----------------------- hello world ------------------------
    @RequestMapping(value = "/a01", method = RequestMethod.GET)
    public String a01() {
        return "Greetings from Spring Boot!";
    }
}
  • wiersz 4: adnotacja [RequestMapping] określa żądanie przetwarzane przez akcję z adnotacją:
    • atrybut [value] to przetwarzana akcja URL,
    • atrybut [method] określa przyjętą metodę;

Zatem metoda [a01] przetwarza żądanie HTTP [GET /a01].

  • wiersz 5: metoda [a01] zwraca typ [String], który zostanie wysłany w niezmienionej postaci do klienta;
  • wiersz 6: zwracany ciąg znaków;

Uruchommy aplikację tak, jak to już wielokrotnie robiliśmy, a następnie z klientem [Advanced Rest Client], żądamy URL [/a01] z GET [1-2]:

  • w [3] – odpowiedź serwera;
  • na [4], nagłówki odpowiedzi HTTP. Widać, że zastosowanym kodowaniem jest [ISO-8859-1]. Można preferować kodowanie UTF-8. Można to skonfigurować;
  • w przypadku [5] wysyłamy to samo zapytanie URL za pomocą przeglądarki Chrome;

Dodajemy następującą akcję [/a02] do kontrolera [ActionsController] (w ten sposób czasami można pomylić URL z metodą, która ją obsługuje pod nazwą akcji):


    // ----------------------- znaki z akcentami - UTF8 ------------------------
    @RequestMapping(value = "/a02", method = RequestMethod.GET, produces="text/plain;charset=UTF-8")
    public String a02() {
        return "caractères accentués : éèàôûî";
}
  • wiersz 2: atrybut [produces="text/plain;charset=UTF-8"] wskazuje, że akcja wysyła strumień tekstu zawierający znaki zakodowane w formacie [UTF-8]. Format ten umożliwia w szczególności stosowanie znaków akcentowanych;

Aby uwzględnić tę nową akcję, musimy ponownie uruchomić aplikację:

 

Wynik jest następujący:

  • w formacie [1] widać charakter dokumentu wysłanego przez serwer;
  • w [2-3] widoczne są znaki z akcentami;

3.3. [/a03]: przekształcenie strumienia XML

Dodajemy następującą akcję [/a03]:


    // ----------------------- text/xml ------------------------
    @RequestMapping(value = "/a03", method = RequestMethod.GET, produces = "text/xml;charset=UTF-8")
    public String a03() {
        String greeting = "<greetings><greeting>Greetings from Spring Boot!</greeting></greetings>";
        return greeting;
}
  • wiersz 2: atrybut [produces="text/xml;charset=UTF-8"] wskazuje, że akcja wysyła strumień XML z znakami zakodowanymi w formacie [UTF-8];

Jej wykonanie daje następujący wynik:

  • w formacie [1] nagłówek HTTP określa, że wysłany dokument jest w formacie HTML;
  • w przypadku [2] przeglądarka Chrome wykorzystuje tę informację do sformatowania otrzymanego tekstu XML;

Przypomnijmy, że w przeglądarce Chrome mamy dostęp do wymiany danych HTTP między klientem a serwerem w oknie deweloperskim (Ctrl-Shift-I):

Image

Od tej pory nie będziemy już rutynowo wykonywać zrzutów ekranu z komunikacji HTTP między klientem a serwerem. Czasami wystarczy po prostu podać treść tej komunikacji.

3.4. [/a04, /a05]: wysłanie strumienia jSON

Dodajemy następującą akcję [/a04]:


    // ----------------------- generowanie jSON ------------------------
    @RequestMapping(value = "/a04", method = RequestMethod.GET)
    public Map<String, Object> a04() {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("1", "un");
        map.put("2", new int[] { 4, 5 });
        return map;
}
  • wiersz 3: akcja zwraca typ [Map], czyli słownik. Przypomnijmy, że w przypadku kontrolera typu [@RestController] wynikiem akcji jest odpowiedź wysyłana do klienta. Ponieważ protokół HTTP służy do wymiany wierszy tekstu, odpowiedź klienta musi zostać zserializowana do postaci ciągu znaków. W tym celu Spring MVC wykorzystuje różne konwertery [Objet <---> chaîne de caractères]. Powiązanie konkretnego obiektu z konwerterem odbywa się poprzez konfigurację. W tym przypadku funkcja autokonfiguracji Spring Boot sprawdzi zależności projektu:
 

Powyższe zależności Jackson to biblioteki służące do serializacji i deserializacji obiektów na ciągi znaków jSON. Spring Boot wykorzysta następnie te biblioteki do serializacji i deserializacji obiektów zwracanych przez akcje. Przykład kodu Java służącego do serializacji i deserializacji obiektów Java w formacie jSON znajduje się w rozdziale 9.7.

Warto zauważyć, że w linii 2 nie podaliśmy typu wysyłanej odpowiedzi. Sprawdzimy, jaki typ zostanie wysłany domyślnie.

W przeglądarce Chrome wyniki są następujące: [1-3]:

Dodajmy teraz następującą akcję: [/a05]:


    // ----------------------- generowanie jSON - 2 ------------------------
    @RequestMapping(value = "/a05", method = RequestMethod.GET)
    public Personne a05() {
        return new Personne(1,"carole",45);
}

Klasa [Personne] wygląda następująco:

  

package istia.st.sprinmvc.models;

public class Personne {

    // identyfikator
    private Integer id;
    // nazwisko
    private String nom;
    // wiek
    private int age;

    // konstruktorzy
    public Personne() {

    }

    public Personne(String nom, int age) {
        this.nom = nom;
        this.age = age;
    }

    public Personne(Integer id, String nom, int age) {
        this(nom, age);
        this.id = id;
    }

    @Override
    public String toString() {
        return String.format("[id=%s, nom=%s,  age=%d]", id, nom, age);
    }

    // metody pobierające i ustawiające
...
}

Wyniki wykonania są następujące:

  • w przypadku [1] serwer wskazuje, że wysyłany dokument to jSON;
  • w przypadku [2] – otrzymano dokument jSON;

3.5. [/a06]: zwracanie pustego strumienia

Dodajemy następującą akcję [/a06]:


    // ----------------------- zwracanie pustego strumienia ------------------------
    @RequestMapping(value = "/a06")
    public void a06() {
}
  • w wierszu 3 akcja [/a06] nie zwraca żadnej wartości. Spring MVC wygeneruje wówczas pustą odpowiedź dla klienta;

Wykonanie daje następujące wyniki:

 

Powyżej atrybut HTTP [Content-Length] w odpowiedzi wskazuje, że serwer wysyła pusty dokument.

3.6. [/a07, /a08, /a09]: rodzaj strumienia z [Content-Type]

Dodajemy następującą akcję [/a07]:


    // ----------------------- text/html ------------------------
    @RequestMapping(value = "/a07", method = RequestMethod.GET, produces = "text/html;charset=UTF-8")
    public String a07() {
        String greeting = "<h1>Greetings from Spring Boot!</h1>";
        return greeting;
}
  • w wierszu 2 akcja [/a07] generuje strumień HTML [text/html];
  • wiersz 4: ciąg znaków HTML;

Wykonanie daje następujące wyniki:

  • w [1] widać, że Chrome zinterpretował tag HTML <h1>, który wyświetla jego zawartość dużymi literami;

Teraz zróbmy to samo z następującą akcją [/a08]:


    // ----------------------- wynik HTML w formacie text/plain ------------------------
    @RequestMapping(value = "/a08", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String a08() {
        String greeting = "<h1>Greetings from Spring Boot!</h1>";
        return greeting;
}
  • wiersz 2: odpowiedź akcji ma typ [text/plain];

Wyniki są następujące:

  • w przypadku [1] przeglądarka Chrome nie zinterpretowała tagu HTML <h1>, ponieważ serwer poinformował ją, że wysyła strumień [text/plain] [2];

Spróbujmy ponownie wykonać podobną operację z następującą akcją [/a09]:


    // ----------------------- wynik HTML w formacie text/xml ------------------------
    @RequestMapping(value = "/a09", method = RequestMethod.GET, produces = "text/xml;charset=UTF-8")
    public String a09() {
        String greeting = "<h1>Greetings from Spring Boot!</h1>";
        return greeting;
}
  • wiersz 2: wysyłamy strumień typu [text/xml];

Wyniki są następujące:

  • w przypadku [1] przeglądarka Chrome nie zinterpretowała tagu HTML <h1>, ponieważ serwer poinformował ją, że wysyła strumień [text/xml] [2]. W związku z tym potraktował tag <h1> jako tag XML;

Z tych przykładów wynika, jak ważny jest nagłówek HTTP [Content-Type] w odpowiedzi serwera. Przeglądarka wykorzystuje ten nagłówek, aby wiedzieć, jak zinterpretować otrzymany dokument;

3.7. [/a10, /a11, /a12]: przekierowanie klienta

Tworzymy nowy kontroler [RedirectController]:

 

Kod dla [RedirectCntroller] będzie na razie wyglądał następująco:


package istia.st.springmvc.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class RedirectController {
}
  • wiersz 7: używamy adnotacji [@Controller], co sprawia, że od tej pory domyślnie typ [String] wyniku akcji oznacza nazwę akcji lub widoku;

Tworzymy następującą akcję [/a10]:


    // ------------ przekierowanie do akcji strony trzeciej -----------------------
    @RequestMapping(value = "/a10", method = RequestMethod.GET)
    public String a10() {
        return "a01";
}
  • wiersz 4: jako wynik zwracamy „a01”, czyli nazwę akcji. To właśnie ona wyśle odpowiedź do klienta;

Oto przykład:

  • w akcji [2] otrzymaliśmy strumień danych z akcji [/a01];
  • w przypadku [3] przeglądarka wyświetla URL akcji [/a10];

Teraz tworzymy następującą akcję o nazwie [/a11]:


    // ------------ tymczasowe przekierowanie 302 do akcji strony trzeciej -----------------------
    @RequestMapping(value = "/a11", method = RequestMethod.GET)
    public String a11() {
        return "redirect:/a01";
}

Otrzymujemy następujące wyniki:

  • w logach przeglądarki Chrome [1-2] widoczne są dwa żądania: jedno skierowane do [/a11], drugie do [/a01];
  • w przypadku [3] serwer odpowiada kodem [302], który nakazuje przeglądarce klienckiej przekierowanie się doURL wskazany w nagłówku HTTP [Location:] [4]. Kod [302] jest kodem przekierowania tymczasowego;

Następnie przeglądarka wysyła drugie żądanie do serwera przekierowującego o numerze URL:

  • do adresu [5], co stanowi drugie żądanie klienta;
  • do [6], przeglądarka klienta wyświetla URL z żądania przekierowania;

Można chcieć wskazać przekierowanie stałe, w którym to przypadku należy wysłać do klienta następujący nagłówek HTTP:

HTTP/1.1 301 Moved Permanently 

co oznacza, że przekierowanie jest stałe. Ta różnica między przekierowaniem tymczasowym (302) a stałym (301) jest uwzględniana przez niektóre wyszukiwarki.

Piszemy akcję [/a12], która spowoduje to stałe przekierowanie:


    // ------------ stałe przekierowanie 301 do akcji strony trzeciej----------------
    @RequestMapping(value = "/a12", method = RequestMethod.GET)
    public void a12(HttpServletResponse response) {
        response.setStatus(301);
        response.addHeader("Location", "/a01");
}
  • wiersz 3: prosimy Spring o wstrzyknięcie obiektu MVC, który zawiera odpowiedź wysyłaną do klienta;
  • wiersz 4: ustalamy [status] odpowiedzi, [301] nagłówka oraz HTTP:
HTTP/1.1 301 Moved Permanently
  • wiersz 5: ręcznie tworzymy następujący nagłówek HTTP:
Location: /a01 

który jest nagłówkiem przekierowującym URL.

Wynikiem wykonania są następujące dane:

Z tego przykładu warto zapamiętać, w jaki sposób:

  • wygenerować status odpowiedzi HTTP;
  • dołączyć nagłówek HTTP do odpowiedzi;

3.8. [/a13]: wygenerować pełną odpowiedź

Możliwe jest pełne kontrolowanie odpowiedzi, co pokazuje poniższa akcja klasy [ResponsesController]:

  

    // ----------------------- pełne wygenerowanie odpowiedzi ------------------------
    @RequestMapping(value = "/a13")
    public void a13(HttpServletResponse response) throws IOException {
        response.setStatus(666);
        response.addHeader("header1", "qq chose");
        response.addHeader("Content-Type", "text/html;charset=UTF-8");
        String greeting = "<h1>Greetings from Spring Boot!</h1>";
        response.getWriter().write(greeting);
}
  • wiersz 3: wynikiem akcji jest [void]. W tym przypadku, aby wysłać do klienta odpowiedź niepustą, należy użyć obiektu [HttpServletResponse response] dostarczonego przez Spring MVC;
  • wiersz 4: nadajemy odpowiedzi status, który nie zostanie rozpoznany przez klienta;
  • wiersz 5: dodajemy nagłówek HTTP, który nie zostanie rozpoznany przez klienta;
  • wiersz 6: dodajemy nagłówek HTTP [Content-Type] w celu określenia typu strumienia, który zostanie wysłany, w tym przypadku HTML;
  • wiersze 7–8: dokument, który będzie następował po nagłówkach HTTP w odpowiedzi;

Wyniki są następujące:

  • w [1] rozpoznajemy elementy naszej odpowiedzi;
  • w [2-3] widać, że Chrome zignorował fakt, iż:
    • status odpowiedzi HTTP nie był rozpoznanym statusem HTTP,
    • nagłówek [header1] nie był rozpoznawalnym nagłówkiem HTTP;

Jeśli klientem nie jest przeglądarka, lecz klient programowy, można dowolnie używać dowolnych statusów i nagłówków.