Skip to content

15. [TD]: utworzenie klienta dla serwisu internetowego

Słowa kluczowe: architektura wielowarstwowa, Spring, wstrzykiwanie zależności, serwis internetowy / jSON, klient / serwer

15.1. Support

  

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

15.2. Architektura klient–serwer

Chcemy stworzyć następującą architekturę klient-serwer:

Warstwa [ui] będzie tą samą, którą opracowaliśmy już w punktach 9 i 10. Będzie to możliwe, ponieważ powyższa warstwa [métier] będzie implementować ten sam interfejs [IElectionsMetier], co warstwa [métier] z punktu 8:


package elections.client.metier;

import elections.client.entities.ListeElectorale;

public interface IElectionsMetier {

    // pobieranie list biorących udział w konkursie
    public ListeElectorale[] getListesElectorales();

    // liczba mandatów do obsadzenia
    public int getNbSiegesAPourvoir();

    // próg wyborczy
    public double getSeuilElectoral();

    // rejestracja wyników
    public void recordResultats(ListeElectorale[] listesElectorales);

    // obliczenie liczby mandatów
    public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);

}

W akapicie 7 warstwa [DAO] wymieniała dane z warstwą SGBD. Tutaj warstwa [DAO] wymienia dane z serwerem WWW / jSON.

Na początek skupimy się na następującej architekturze:

15.3. Projekt Eclipse

Projekt Eclipse wygląda następująco:

Struktura ta jest identyczna jak w przykładowym projekcie z paragrafu 13.6.1. Postępujemy w ten sam sposób.

15.4. Konfiguracja Maven

Jest to konfiguracja opisana w punkcie 13.6.2.

15.5. Implementacja warstwy [DAO]

  
  • pakiet [elections.client.config] zawiera konfigurację Spring dla warstwy [DAO];
  • pakiet [elections .client.dao] zawiera implementację warstwy [DAO];
  • pakiet [elections .client.entities] zawiera obiekty wymieniane z usługą internetową / jSON;
  • pakiet [elections .client.metier] zawiera warstwę [métier]
  • pakiet [elections .client.ui] zawiera warstwę [UI]

15.5.1. Konfiguracja warstwy [métier]

  

Klasa [MetierConfig] odpowiada za konfigurację warstwy [métier] w Spring. Jej kod wygląda następująco:


package elections.client.config;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Scope;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.databind.ObjectMapper;

@ComponentScan({ "elections.client.dao","elections.client.metier" })
public class MetierConfig {

    // stałe
    static private final int TIMEOUT = 1000;
    static private final String URL_WEBJSON = "http://localhost:8080";

    @Bean
    public RestTemplate restTemplate(int timeout) {
        // utworzenie komponentu RestTemplate
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // limit czasu wymiany danych
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
        // wynik
        return restTemplate;
    }

    @Bean
    public int timeout() {
        return TIMEOUT;
    }

    @Bean
    public String urlWebJson() {
        return URL_WEBJSON;
    }

    // mapper jSON
    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }
}

Kod ten został wyjaśniony w paragrafie 13.6.3.1. Jest on prostszy, ponieważ nie ma tu filtrów jSON, którymi należałoby się zająć.

15.5.2. Entities

  

Obiekty przetwarzane przez warstwy [DAO] i [métier] to te, które są wymieniane z usługą internetową / jSON. Są to obiekty typu [ElectionsConfig] i [ListeElectorale]. Po stronie serwera encje te posiadały adnotacje trwałości JPA. W tym przypadku adnotacje te zostały usunięte. Dla przypomnienia podajemy kod encji:

[AbstractEntity]


package spring.webjson.client.entities;


public abstract class AbstractEntity {
    // właściwości
    protected Long id;
    protected Long version;

    // konstruktorzy
    public AbstractEntity() {

    }

    public AbstractEntity(Long id, Long version) {
        this.id = id;
        this.version = version;
    }

    // przedefiniowanie [equals] i [hashcode]
    @Override
    public int hashCode() {
        return (id != null ? id.hashCode() : 0);
    }

    @Override
    public boolean equals(Object entity) {
        if (!(entity instanceof AbstractEntity)) {
            return false;
        }
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return id != null && this.id == other.id.longValue();
    }

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

[ElectionsConfig]


package elections.webjson.client.entities;


public class ElectionsConfig extends AbstractEntity {

    // pola
    private int nbSiegesAPourvoir;
    private double seuilElectoral;

    // konstruktory
    public ElectionsConfig() {

    }

    public ElectionsConfig(int nbSiegesAPourvoir, double seuilElectoral) {
        this.nbSiegesAPourvoir = nbSiegesAPourvoir;
        this.seuilElectoral = seuilElectoral;
    }

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

[ListeElectorale]


package elections.webjson.client.entities;


public class ListeElectorale extends AbstractEntity {

    // pola
    private String nom;
    private int voix;
    private int sieges;
    private boolean elimine;

    // konstruktory
    public ListeElectorale() {
    }

    public ListeElectorale(String nom, int voix, int sieges, boolean elimine) {
        setNom(nom);
        setVoix(voix);
        setSieges(sieges);
        setElimine(elimine);
    }

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

15.5.3. Interfejs warstwy [DAO]

  

Warstwa [DAO] posiada następujący interfejs [IClientDao]:


package elections.client.dao;

public interface IClientDao {

    // zapytanie ogólne
    String getResponse(String url, String jsonPost);

}

Interfejs posiada tylko jedną metodę [getResponse]:

  • pierwszym parametrem jest adres serwera, do którego ma zostać wysłane zapytanie;
  • drugim parametrem jest wartość jSON wartości, którą należy wysłać, a null, jeśli nie ma nic do wysłania;
  • wynikiem jest ciąg znaków jSON obiektu [Response<T>], którego klasa [Response] została opisana w paragrafie 14.7;

15.5.4. Implementacja wymiany danych z serwisem internetowym / jSON

  

Klasa [ClientDao] implementuje interfejs [IClientDao] w następujący sposób:


package elections.client.dao;

import java.net.URI;
import java.net.URISyntaxException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import elections.client.entities.ElectionsException;

@Component
public class ClientDao implements IClientDao {

    // dane
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    // zapytanie ogólne
    @Override
    public String getResponse(String url, String jsonPost) {

        try {
            // adres URL: URL – prosimy o kontakt
            // jsonPost: wartość jSON do wysłania

            // wykonanie żądania
            RequestEntity<?> request;
            if (jsonPost != null) {
                // zapytanie POST
                request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
            } else {
                // zapytanie GET
                request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON).build();
            }
            // wykonuje się zapytanie
            return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
            }).getBody();
        } catch (URISyntaxException e1) {
            throw new ElectionsException(200, e1);
        } catch (RuntimeException e2) {
            throw new ElectionsException(201, e2);
        }
    }
}

Kod ten został opisany w paragrafie 13.6.3.6.

15.6. Implementacja warstwy [métier]

 

Jak wspomniano, warstwa [métier] posiada ten sam interfejs [IElectionsMetier], co w paragrafie 8.4:


package elections.client.metier;

import elections.client.entities.ListeElectorale;

public interface IElectionsMetier {

    // pobieramy listy startowe
    public ListeElectorale[] getListesElectorales();

    // liczba mandatów do obsadzenia
    public int getNbSiegesAPourvoir();

    // próg wyborczy
    public double getSeuilElectoral();

    // rejestracja wyników
    public void recordResultats(ListeElectorale[] listesElectorales);

    // obliczenie liczby mandatów
    public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);

}

Interfejs ten jest zaimplementowany przez następującą klasę [ElectionsMetier]:


package elections.client.metier;

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

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import elections.client.dao.IClientDao;
import elections.client.entities.ElectionsConfig;
import elections.client.entities.ElectionsException;
import elections.client.entities.ListeElectorale;

@Component
public class ElectionsMetier implements IElectionsMetier {

    @Autowired
    private IClientDao dao;
    @Autowired
    private ApplicationContext context;

    // konfiguracja wyborów
    private ElectionsConfig electionsConfig;

    @PostConstruct
    public void init() {
        // mapery jSON
        ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
        try {
            // zapytanie
            Response<ElectionsConfig> response = mapperResponse.readValue(dao.getResponse("/getElectionsConfig", null),
                    new TypeReference<Response<ElectionsConfig>>() {
                    });
            // błąd?
            if (response.getStatus() != 0) {
                // wyzwalany jest 1 wyjątek
                throw new ElectionsException(response.getStatus(), response.getMessages());
            } else {
                electionsConfig = response.getBody();
            }
        } catch (ElectionsException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new ElectionsException(100, getMessagesForException(e2));
        }
    }

    @Override
    public ListeElectorale[] getListesElectorales() {
        ...
    }

    @Override
    public int getNbSiegesAPourvoir() {
        return electionsConfig.getNbSiegesAPourvoir();
    }

    @Override
    public double getSeuilElectoral() {
        return electionsConfig.getSeuilElectoral();
    }

    @Override
    public void recordResultats(ListeElectorale[] listesElectorales) {
    ...
    }

    @Override
    public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales) {
...
    }

    // lista komunikatów o błędach związanych z wyjątkiem
    private List<String> getMessagesForException(Exception exception) {
        // pobierana jest lista komunikatów o błędach wyjątku
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        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;
    }
}

Typ [Response] użyty w wierszu 37 to odpowiedź serwera WWW / jSON opisana w paragrafie 14.7;


Zadanie do wykonania: zgodnie z paragrafem 13.6.3.7 uzupełnij klasę [ElectionsMetier];


15.7. Test Junit

Wróćmy do budowanej obecnie architektury klient-serwer:

Warstwa [JUnit] [1] komunikuje się z warstwą [Métier] serwera [5] poprzez warstwy [2-4]. Dzięki zapewnieniu, że warstwy [Métier], [2] i [5] mają ten sam interfejs, warstwy [2-4] stają się przezroczyste. Warstwa [1] sprawia wrażenie, jakby komunikowała się bezpośrednio z warstwą [5]. Ciekawe jest to, że w warstwie [1] będziemy mogli wykorzystać test JUnit, który został użyty do przetestowania warstw [Métier] i [5].

  

Zadanie do wykonania: przeprowadź test JUnit projektu, aby sprawdzić implementację zarówno serwera, jak i klienta.


15.8. Implementacja warstwy [UI]

Wróćmy do architektury, którą chcemy zbudować:

Teraz, gdy warstwa [métier] [2] została zbudowana i przetestowana, możemy zbudować warstwę [ui] [1].

Ponieważ interfejs [IElectionsMetier] warstwy [métier] jest identyczny z interfejsem projektu opisanego w akapicie 8, możemy w [3] skopiować projekt z warstwy [ui] z punktu 10. Był to projekt NetBeans. Wystarczy skopiować i wkleić odpowiednie klasy Java z NetBeans do Eclipse. Po wykonaniu tej czynności należy wprowadzić poprawki dotyczące pakietów i importów.

To samo zrobimy w przypadku klas wykonywalnych z pakietów [elections.client.boot] i [4].

Klasa [AbstractBootElections] ma następującą postać:


package elections.client.boot;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import elections.client.config.UiConfig;
import elections.client.entities.ElectionsException;
import elections.client.ui.IElectionsUI;

public abstract class AbstractBootElections {

    // pobieranie kontekstu Spring
    protected AnnotationConfigApplicationContext ctx;

    public void run() {
        // instancjonowanie warstwy [ui]
        IElectionsUI electionsUI = null;
        try {
            // pobieranie kontekstu Spring
            ctx = new AnnotationConfigApplicationContext(UiConfig.class);
            // pobieranie warstwy [ui]
            electionsUI = getUI();

...
  • wiersz 19: kontekst Spring zdefiniowany przez klasę konfiguracyjną [UiConfig] jest instancjonowany. Klasa ta wygląda następująco:
  

Klasa [UiConfig] ma następującą postać:


package elections.client.config;

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

@Import(MetierConfig.class)
@ComponentScan(basePackages = { "elections.client.ui" })
public class UiConfig {
}
  • wiersz 6: importowane są bean'y z warstwy [métier];
  • wiersz 7: wskazujemy, że w pakiecie [elections.client.ui] znajdują się bean'y Spring;

Zadanie do wykonania: sprawdź, czy wersje konsolowa i Swing warstwy [ui] działają.