Skip to content

14. [TD]: Udostępnienie warstwy [metier] w sieci

Słowa kluczowe: architektura wielowarstwowa, Spring, wstrzykiwanie zależności, usługa internetowa / jSON, klient / serwer.

Wróćmy do obecnej architektury aplikacji TD:

Zamierzamy przekształcić tę architekturę w następującą:

w celu udostępnienia w sieci interfejsu [IMetier] warstwy biznesowej. W tym celu zastosujemy metodologię opisaną w paragrafie 13.5.

14.1. Support

  

Projekty z tego rozdziału znajdują się w folderze [support / chap-14].

14.2. Projekt Eclipse warstwy [métier]

  

14.2.1. Konfiguracja Maven

Projekt warstwy [métier] jest projektem Maven skonfigurowanym za pomocą następującego pliku [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.elections</groupId>
    <artifactId>elections-metier-dao-spring-data</artifactId>
    <version>0.1.0</version>

    <!-- zależności -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>
    <dependencies>
        <!-- warstwa [DAO] -->
        <dependency>
            <groupId>istia.st.elections</groupId>
            <artifactId>elections-dao-spring-data-01</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Test Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <properties>
        <!-- użyj UTF-8 do wszystkiego -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
</project>
  • wiersze 18–22: zależność od warstwy [DAO] utworzonej w punkcie 12;
  • wiersze 23–34: zależności niezbędne do przeprowadzenia testów;

14.2.2. Konfiguracja Spring

  

Projekt warstwy [métier] jest projektem Spring skonfigurowanym przez następujący plik [MetierConfig]:


package elections.metier.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;

import elections.dao.config.DaoConfig;

@Import({ DaoConfig.class })
@ComponentScan({ "elections.metier.service" })
public class MetierConfig {
}
  • Nie stosujemy tutaj notacji [@Configuration], która sprawia, że klasa staje się klasą konfiguracyjną Springa. Obecność adnotacji [@Import, @ComponentScan] automatycznie czyni ją klasą konfiguracyjną;
  • wiersz 8: importujemy plik konfiguracyjny warstwy [DAO]. Uzyskujemy w ten sposób dostęp do wszystkich beanów zdefiniowanych w tym pliku;
  • wiersz 9: pozostałe komponenty Spring należy pobrać z folderu [elections.metier.service];

14.2.3. Implementacja warstwy [métier]

  

Implementacja warstwy [métier] jest zgodna z opisem w paragrafie 8.5.

14.2.4. Test warstwy [métier]

  

Klasa testowa jest zgodna z opisem zawartym w paragrafie 8.6.


Zadanie do wykonania: zaimplementuj projekt warstwy [métier] i przeprowadź jej test jednostkowy. Wygeneruj archiwum warstwy w lokalnym repozytorium Maven (run as / Maven / install).


14.3. Projekt Eclipse warstwy [web]

Warstwa internetowa to warstwa Spring MVC:

Projekt Eclipse ma następującą strukturę:

  • [Boot.java] to klasa uruchamiająca usługę internetową;
  • [WebConfig.java] to klasa konfiguracyjna serwisu internetowego;
  • [Response.java] to odpowiedź generowana przez różne instancje klasy URL serwisu internetowego;
  • [ElectionsController] to klasa implementacyjna serwisu internetowego;

14.4. Konfiguracja Maven

Projekt jest projektem Maven skonfigurowanym za pomocą następującego pliku [pom.xml]:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.elections</groupId>
    <artifactId>elections-webjson-metier-dao-spring-data</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>elections-webjson-metier-dao-spring-data</name>
    <description>couche métier exposée comme un service web / jSON</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <dependencies>
        <!-- warstwa biznesowa -->
        <dependency>
            <groupId>istia.st.elections</groupId>
            <artifactId>elections-metier-dao-spring-data</artifactId>
            <version>0.1.0</version>
        </dependency>
        <!-- warstwa MVC -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • wiersze 19–23: zależność od archiwum warstwy [métier]. Jest to archiwum, które utworzyliśmy w punkcie 14;
  • wiersze 25–28: zależność od biblioteki Spring o nazwie MVC;

14.5. Konfiguracja Spring

 

Klasa [WebConfig] konfiguruje usługę internetową:


package elections.webjson.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Scope;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import com.fasterxml.jackson.databind.ObjectMapper;

import elections.metier.config.MetierConfig;

@EnableWebMvc
@Import({ MetierConfig.class })
@ComponentScan({ "elections.webjson.service" })
public class WebConfig {
    // -------------------------------- konfiguracja warstwy [web]
    @Autowired
    private ApplicationContext context;

    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
        return servlet;
    }

    @Bean
    public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new ServletRegistrationBean(dispatcherServlet, "/*");
    }

    @Bean
    public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
        return new TomcatEmbeddedServletContainerFactory("", 8080);
    }
    // mapper jSON
    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }

}
  • znaczenie tej konfiguracji zostało wyjaśnione w paragrafie 13.5.3.1. Wyjaśniamy jedynie nowości:
  • wiersz 22: importujemy plik konfiguracyjny warstwy [métier], aby móc korzystać ze wszystkich jej beanów;
  • wiersz 23: wskazujemy, że inne komponenty (beans) znajdują się w folderze [elections.webjson.server.service];

14.6. Klasa uruchamiająca usługę internetową

 

Klasa [Boot] uruchamia usługę internetową w następujący sposób:


package elections.webjson.boot;

import org.springframework.boot.SpringApplication;

import elections.webjson.config.WebConfig;

public class Boot {

    public static void main(String[] args) {
        SpringApplication.run(WebConfig.class, args);
    }
}
  • wiersz 10: metoda statyczna [SpringApplication.run] wykorzysta plik konfiguracyjny [WebConfig]. Dzięki adnotacji [@EnableAutoConfiguration] Spring Boot uruchomi serwer Tomcat i wdroży na nim usługę internetową;

14.7. Odpowiedź serwisu internetowego URL

 

Wszystkie URL serwisu internetowego / jSON wysyłają ten sam typ odpowiedzi:


package elections.webjson.service;

import java.util.List;

public class Response<T> {

    // ----------------- właściwości
    // status operacji
    private int status;
    // ewentualne komunikaty o błędach
    private List<String> messages;
    // treść odpowiedzi
    private T body;

    // konstruktory
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

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

Klasa ta została przedstawiona i omówiona w paragrafie 13.5.5.3.

14.8. Implementacja usługi internetowej / jSON

 

Usługa internetowa / jSON jest zaimplementowana przez następującą klasę [ElectionsController]:


package elections.webjson.service;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import elections.dao.entities.ElectionsConfig;
import elections.dao.entities.ElectionsException;
import elections.metier.service.IElectionsMetier;

@Controller
public class ElectionsController {

    // zależności Spring
    @Autowired
    private ObjectMapper jsonMapper;

    @Autowired
    private IElectionsMetier metier;

    @RequestMapping(value = "/getElectionsConfig", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getElectionsConfig() throws JsonProcessingException {
        // odpowiedź
        Response<ElectionsConfig> response;
        try {
            response = new Response<>(0, null,
                    new ElectionsConfig(metier.getNbSiegesAPourvoir(), metier.getSeuilElectoral()));
        } catch (ElectionsException e1) {
            response = new Response<>(e1.getCode(), e1.getErreurs(), null);
        } catch (RuntimeException e2) {
            response = new Response<>(1000, getErreursForException(e2), null);
        }
        // odpowiedź
        return jsonMapper.writeValueAsString(response);
    }

    @RequestMapping(value = "/getListesElectorales", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getListesElectorales() throws JsonProcessingException {
        throw new UnsupportedOperationException("Not supported yet");
    }

    @RequestMapping(value = "/setListesElectorales", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String setListesElectorales(HttpServletRequest request) throws JsonProcessingException {
        throw new UnsupportedOperationException("Not supported yet");
    }

    @RequestMapping(value = "/calculerSieges", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String calculerSieges(HttpServletRequest request) throws JsonProcessingException {
        throw new UnsupportedOperationException("Not supported yet");
    }

    // metody prywatne -----------------------------
    // lista komunikatów o błędach z wyjątku RuntimeException
    private List<String> getErreursForException(Exception e) {
        // pobieramy listę komunikatów o błędach z wyjątku
        Throwable cause = e;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // pobieramy komunikat tylko wtedy, gdy !=null i nie jest pusty
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // następna przyczyna
            cause = cause.getCause();
        }
        return erreurs;
    }

}

Zadanie: postępując zgodnie z instrukcjami zawartymi w paragrafie 13.5.5, uzupełnij kod klasy [ElectionsController].


Uwagi:

  • nie ma tutaj filtrów jSON, ponieważ tabele [CONF] i [LISTES] nie są ze sobą powiązane relacją klucza obcego, co znacznie upraszcza kod serwisu internetowego;
  • nie należy zapomnieć o różnych niezbędnych adnotacjach Spring;
  • metodom URL nadamy nazwy powiązanych metod;
  • metoda [setListeElectorales] jest wywoływana wraz z operacją [POST]. Wartością wysyłaną jest tablica konkurujących list (typu ListeElectorale[]) wraz z ich atrybutami [sieges, voix, elimine], które należy zapisać w bazie danych. Metoda ta zwraca typ [Response<Void>] z polem [status=0], jeśli nie wystąpił żaden błąd, w przeciwnym razie zwraca inną wartość;
  • metoda [calculerSieges] jest wywoływana wraz z operacją [POST]. Przesyłaną wartością jest tablica list wyborczych (typu ListeElectorale[]) wraz z ich atrybutami [nom, voix]. Metoda ta zwraca typ [Response<ListeElectorale[]>], którego treścią są listy wyborcze z zainicjowanymi polami [sieges, elimine];

14.9. Tests

Po uruchomieniu serwisu internetowego należy przeprowadzić następujące testy, aby upewnić się, że serwis działa poprawnie przy użyciu narzędzia [Advanced Rest Client]:

 

Odpowiedź jSON na poprzednie żądanie [1] brzmi następująco:

1
2

W [2] skopiuj odpowiedź do schowka, a następnie wklej ją do dowolnego edytora tekstu [3]:

Wyodrębnij wartość pola [body] i zmień na przykład liczbę głosów na listach. Poniżej, w pliku [4], ustawiono liczbę głosów na wszystkich listach na 100:

Sprawdź, czy Twój ciąg znaków jSON zaczyna się od [ et se termine par ]. Znaki te służą do wyznaczenia granic tablicy jSON. W polu [5] wklej powyższy ciąg znaków jSON. Będzie to wartość wysyłana dla następnego pola URL. W tym celu należy wybrać metodę HTTP [POST] [7].

  • w [6] należy zażądać URL oraz [setListesElectorales]. Ten URL jest żądany wraz z POST. Wartością wprowadzoną jest tabela jSON zawierająca listy zawodników, których wyniki należy zapisać w bazie danych;

Otrzymujemy następujący wynik:

 

Pole [status=0] wskazuje, że nie wystąpił żaden błąd. Aby to sprawdzić, należy ponownie zażądać list konkursowych i upewnić się, że wprowadzone zmiany zostały uwzględnione:

Ponownie generujemy [POST], aby obliczyć liczbę mandatów uzyskanych przez listy:

  • w [1]: URL z obliczeń mandatów;
  • w [2]: tworzymy plik [POST];
  • w [3]: listy startujące w wyborach. W polu [voix] wpisujemy wartości z pola TD, wszystkie pola [sieges] przyjmują wartość 0, a wszystkie pola [elimine] przyjmują wartość false;

Otrzymany wynik jest następujący: