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ą.















