Skip to content

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] працюють.