15. [TD]: створення клієнта для веб-сервісу
Ключові слова: багаторівнева архітектура, Spring, ін’єкція залежностей, веб-сервіс / jSON, клієнт / сервер
15.1. Support
![]() |
Проєкти цього розділу можна знайти в папці [support / chap-15].
15.2. Архітектура «клієнт/сервер»
Ми хочемо створити таку архітектуру «клієнт/сервер»:
![]() |
Рівень [ui] — це той, що вже був розроблений у параграфах 9 та 10. Це стане можливим, оскільки вищезазначений рівень [métier] реалізує той самий інтерфейс [IElectionsMetier], що й рівень [métier] з пункту 8:
package elections.client.metier;
import elections.client.entities.ListeElectorale;
public interface IElectionsMetier {
// отримання списків-конкурентів
public ListeElectorale[] getListesElectorales();
// кількість місць, що підлягають заповненню
public int getNbSiegesAPourvoir();
// виборчий бар'єр
public double getSeuilElectoral();
// реєстрація результатів
public void recordResultats(ListeElectorale[] listesElectorales);
// розподіл місць
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
}
У параграфі 7 рівень [DAO] обмінювався даними з SGBD. Тут рівень [DAO] обмінюється даними з веб-сервером / jSON.
Спочатку ми розглянемо таку архітектуру:
![]() |
15.3. Проєкт Eclipse
Проект Eclipse виглядає так:
![]() | ![]() | ![]() |
Ця структура повторює структуру прикладу проекту з розділу 13.6.1. Ми будемо дотримуватися того самого підходу.
15.4. Налаштування Maven
Вона описана в розділі 13.6.2.
15.5. Реалізація рівня [DAO]
![]() |
![]() |
- пакет [elections.client.config] містить конфігурацію Spring для шару [DAO];
- пакет [elections .client.dao] містить реалізацію шару [DAO];
- пакет [elections .client.entities] містить об’єкти, що обмінюються з веб-сервісом / jSON;
- пакет [elections .client.metier] містить рівень [métier]
- пакет [elections .client.ui] містить шар [UI]
15.5.1. Налаштування шару [métier]
![]() |
Клас [MetierConfig] здійснює конфігурацію шару [métier] у Spring. Його код такий:
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 {
// константи
static private final int TIMEOUT = 1000;
static private final String URL_WEBJSON = "http://localhost:8080";
@Bean
public RestTemplate restTemplate(int timeout) {
// створення компонента RestTemplate
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// тайм-аут обміну даними
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
// результат
return restTemplate;
}
@Bean
public int timeout() {
return TIMEOUT;
}
@Bean
public String urlWebJson() {
return URL_WEBJSON;
}
// маппер jSON
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
Цей код було пояснено в розділі 13.6.3.1. Він простіший, оскільки тут немає фільтрів jSON, якими потрібно керувати.
15.5.2. Елементи
![]() |
Об'єкти, з якими працюють шари [DAO] та [métier], — це ті, якими вони обмінюються з веб-сервісом /jSON. Це об’єкти типів [ElectionsConfig] та [ListeElectorale]. На стороні сервера ці сутності мали анотації персистентності JPA. Тут ці анотації було видалено. Нагадуємо код цих сутностей:
[AbstractEntity]
package spring.webjson.client.entities;
public abstract class AbstractEntity {
// властивості
protected Long id;
protected Long version;
// конструктори
public AbstractEntity() {
}
public AbstractEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
// перевизначення [equals] та [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();
}
// методи отримання та встановлення
...
}
[ElectionsConfig]
package elections.webjson.client.entities;
public class ElectionsConfig extends AbstractEntity {
// поля
private int nbSiegesAPourvoir;
private double seuilElectoral;
// конструктори
public ElectionsConfig() {
}
public ElectionsConfig(int nbSiegesAPourvoir, double seuilElectoral) {
this.nbSiegesAPourvoir = nbSiegesAPourvoir;
this.seuilElectoral = seuilElectoral;
}
// геттери та сеттери
...
}
[ListeElectorale]
package elections.webjson.client.entities;
public class ListeElectorale extends AbstractEntity {
// поля
private String nom;
private int voix;
private int sieges;
private boolean elimine;
// конструктори
public ListeElectorale() {
}
public ListeElectorale(String nom, int voix, int sieges, boolean elimine) {
setNom(nom);
setVoix(voix);
setSieges(sieges);
setElimine(elimine);
}
// методи getter та setter
...
}
15.5.3. Інтерфейс шару [DAO]
![]() |
Рівень [DAO] має такий інтерфейс [IClientDao]:
package elections.client.dao;
public interface IClientDao {
// загальний запит
String getResponse(String url, String jsonPost);
}
Інтерфейс має лише один метод [getResponse]:
- перший параметр — це URL сервера, до якого слід звернутися;
- другий параметр — це значення jSON, яке потрібно відправити, або null, якщо відправляти нічого немає;
- результатом є рядок jSON об’єкта [Response<T>], клас [Response] якого описано в розділі 14.7;
15.5.4. Реалізація обміну даними з веб-сервісом / jSON
![]() |
Клас [ClientDao] реалізує інтерфейс [IClientDao] наступним чином:
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 {
// дані
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// загальний запит
@Override
public String getResponse(String url, String jsonPost) {
try {
// URL: URL для зв’язку
// jsonPost: значення jSON для відправлення
// виконання запиту
RequestEntity<?> request;
if (jsonPost != null) {
// запит POST
request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
} else {
// запит GET
request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
.accept(MediaType.APPLICATION_JSON).build();
}
// виконується запит
return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
}).getBody();
} catch (URISyntaxException e1) {
throw new ElectionsException(200, e1);
} catch (RuntimeException e2) {
throw new ElectionsException(201, e2);
}
}
}
Цей код було описано в розділі 13.6.3.6.
15.6. Реалізація рівня [métier]
![]() |
Як уже зазначалося, рівень [métier] має той самий інтерфейс [IElectionsMetier], що й у розділі 8.4:
package elections.client.metier;
import elections.client.entities.ListeElectorale;
public interface IElectionsMetier {
// отримати списки кандидатів
public ListeElectorale[] getListesElectorales();
// кількість місць, що підлягають заповненню
public int getNbSiegesAPourvoir();
// виборчий бар'єр
public double getSeuilElectoral();
// реєстрація результатів
public void recordResultats(ListeElectorale[] listesElectorales);
// розподіл місць
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
}
Цей інтерфейс реалізовано наступним класом [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;
// організація виборів
private ElectionsConfig electionsConfig;
@PostConstruct
public void init() {
// маппери jSON
ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
try {
// запит
Response<ElectionsConfig> response = mapperResponse.readValue(dao.getResponse("/getElectionsConfig", null),
new TypeReference<Response<ElectionsConfig>>() {
});
// помилка?
if (response.getStatus() != 0) {
// виникає 1 виняток
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) {
...
}
// список повідомлень про помилки винятку
private List<String> getMessagesForException(Exception exception) {
// отримано список повідомлень про помилки винятку
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
// отримуємо повідомлення, лише якщо воно !=null і не порожнє
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// наступна причина
cause = cause.getCause();
}
return erreurs;
}
}
Тип [Response], що використовується у рядку 37, є відповіддю веб-сервера / jSON, описаною у розділі 14.7;
Завдання: відповідно до розділу 13.6.3.7 доповніть клас [ElectionsMetier];
15.7. Тест Junit
Повернемося до архітектури «клієнт-сервер», яку ми зараз будуємо:
![]() |
Рівень [JUnit] [1] взаємодіє з рівнем [Métier] сервера [5] через рівні [2-4]. Завдяки тому, що шари [Métier], [2] та [5] мають однаковий інтерфейс, шари [2-4] стають прозорими. Рівень [1] створює враження, ніби він безпосередньо взаємодіє з рівнем [5]. Цікаво, що в [1] ми зможемо використати тест JUnit, який раніше застосовувався для тестування шарів [Métier] та [5].
![]() |
Завдання: пройдіть тест JUnit цього проєкту, щоб перевірити свою реалізацію, а також роботу сервера та його клієнта.
15.8. Реалізація шару [UI]
Повернемося до архітектури, яку ми хочемо побудувати:
![]() |
Тепер, коли рівень [métier] [2] побудовано та протестовано, ми можемо побудувати рівень [ui] [1].
![]() |
Оскільки інтерфейс [IElectionsMetier] шару [métier] ідентичний інтерфейсу проекту, описаного в параграфі 8, то в [3] ми можемо скопіювати проект з шару [ui], описаного в параграфі 10. Цей проект був проектом NetBeans. Достатньо просто скопіювати та вставити відповідні Java-класи з NetBeans в Eclipse. Після цього потрібно внести корективи в пакети та імпорти.
Те саме зробимо для виконуваних класів із пакетів [elections.client.boot] та [4].
Клас [AbstractBootElections] має такий вигляд:
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 {
// отримання контексту Spring
protected AnnotationConfigApplicationContext ctx;
public void run() {
// інстанціювання шару [ui]
IElectionsUI electionsUI = null;
try {
// отримання контексту Spring
ctx = new AnnotationConfigApplicationContext(UiConfig.class);
// отримання шару [ui]
electionsUI = getUI();
...
- рядок 19: створюється екземпляр контексту Spring, визначеного класом конфігурації [UiConfig]. Цей клас має такий вигляд:
![]() |
Клас [UiConfig] має такий вигляд:
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 {
}
- рядок 6: імпортуються біни з шару [métier];
- рядок 7: вказується, що в пакеті [elections.client.ui] є біни Spring;
Завдання: переконайтеся, що консольна та Swing-версії шару [ui] працюють.















