Skip to content

15. [TD]: إنشاء عميل لخدمة الويب

الكلمات المفتاحية: بنية متعددة الطبقات، Spring، حقن التبعية، خدمة الويب / JSON، العميل / الخادم

15.1. الدعم

  

يمكن العثور على مشاريع هذا الفصل في المجلد [support / chap-15].

15.2. بنية العميل/الخادم

نريد إنشاء بنية العميل/الخادم التالية:

ستكون طبقة [ui] هي الطبقة التي تم تطويرها بالفعل في القسمين 9 و10. وسيكون ذلك ممكنًا لأن طبقة [business] أعلاها ستنفذ نفس الواجهة [IElectionsMetier] التي تنفذها طبقة [business] في القسم 8:


package elections.client.metier;
 
import elections.client.entities.ListeElectorale;

public interface IElectionsMetier {
 
    // get the lists in competition
    public ListeElectorale[] getListesElectorales();
 
    // the number of seats to be filled
    public int getNbSiegesAPourvoir();
 
    // the electoral threshold
    public double getSeuilElectoral();
 
    // recording results
    public void recordResultats(ListeElectorale[] listesElectorales);
 
    // calculating seats
    public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
 
}

في القسم 7، كانت طبقة [DAO] تتبادل البيانات مع نظام إدارة قواعد البيانات (DBMS). أما هنا، فتتبادل طبقة [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.business] على طبقة [business]
  • تحتوي حزمة [elections.client.ui] على طبقة [UI]

15.5.1. تكوين طبقة [business]

  

تتولى فئة [MetierConfig] تكوين Spring لطبقة [business]. وفيما يلي شفرة البرمجة الخاصة بها:


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 {
 
    // constants
    static private final int TIMEOUT = 1000;
    static private final String URL_WEBJSON = "http://localhost:8080";
 
    @Bean
    public RestTemplate restTemplate(int timeout) {
        // creation of the RestTemplate component
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // exchange timeout
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
        // result
        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();
    }
}

تم شرح هذا الكود في القسم 13.6.3.1. وهو أبسط لأنه لا توجد مرشحات JSON لإدارتها هنا.

15.5.2. الكيانات

  

الكيانات التي تتعامل معها طبقات [DAO] و[business] هي تلك التي يتم تبادلها مع خدمة الويب / JSON. وهي كائنات من النوع [ElectionsConfig] و[VoterList]. على جانب الخادم، كانت هذه الكيانات تحتوي على تعليقات توضيحية خاصة بالاستمرارية في JPA. هنا، تمت إزالة تلك التعليقات التوضيحية. ندرج كود الكيان مرة أخرى كمرجع:

[كيان الملخص]


package spring.webjson.client.entities;
 
 
public abstract class AbstractEntity {
    // properties
    protected Long id;
    protected Long version;
 
    // manufacturers
    public AbstractEntity() {
 
    }
 
    public AbstractEntity(Long id, Long version) {
        this.id = id;
        this.version = version;
    }
 
    // redefine [equals] and [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();
    }
 
    // getters and setters
    ...
}

[ElectionsConfig]


package elections.webjson.client.entities;
 
 
public class ElectionsConfig extends AbstractEntity {
 
    // fields
    private int nbSiegesAPourvoir;
    private double seuilElectoral;
 
    // manufacturers
    public ElectionsConfig() {
 
    }
 
    public ElectionsConfig(int nbSiegesAPourvoir, double seuilElectoral) {
        this.nbSiegesAPourvoir = nbSiegesAPourvoir;
        this.seuilElectoral = seuilElectoral;
    }
 
    // getters and setters
    ...
}

[قائمة الناخبين]


package elections.webjson.client.entities;
 
 
public class ListeElectorale extends AbstractEntity {
 
    // fields
    private String nom;
    private int voix;
    private int sieges;
    private boolean elimine;
 
    // manufacturers
    public ListeElectorale() {
    }
 
    public ListeElectorale(String nom, int voix, int sieges, boolean elimine) {
        setNom(nom);
        setVoix(voix);
        setSieges(sieges);
        setElimine(elimine);
    }
 
    // getters and setters
    ...
}

15.5.3. واجهة طبقة [DAO]

  

تحتوي طبقة [DAO] على واجهة [IClientDao] التالية:


package elections.client.dao;
 
public interface IClientDao {
 
    // generic request
    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 {
 
    // data
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;
 
    // generic request
    @Override
    public String getResponse(String url, String jsonPost) {
 
        try {
            // url : URL to contact
            // jsonPost: the jSON value to be posted
 
            // request execution
            RequestEntity<?> request;
            if (jsonPost != null) {
                // query POST
                request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
            } else {
                // query GET
                request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON).build();
            }
            // execute the query
            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. تنفيذ طبقة [الأعمال]

 

كما ذكرنا سابقًا، فإن طبقة [الأعمال] لها نفس الواجهة [IElectionsMetier] كما في القسم 8.4:


package elections.client.metier;
 
import elections.client.entities.ListeElectorale;
 
public interface IElectionsMetier {
 
    // get the lists in competition
    public ListeElectorale[] getListesElectorales();
 
    // the number of seats to be filled
    public int getNbSiegesAPourvoir();
 
    // the electoral threshold
    public double getSeuilElectoral();
 
    // recording results
    public void recordResultats(ListeElectorale[] listesElectorales);
 
    // calculating seats
    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;
 
    // election configuration
    private ElectionsConfig electionsConfig;
 
    @PostConstruct
    public void init() {
        // mappers jSON
        ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
        try {
            // request
            Response<ElectionsConfig> response = mapperResponse.readValue(dao.getResponse("/getElectionsConfig", null),
                    new TypeReference<Response<ElectionsConfig>>() {
                    });
            // mistake?
            if (response.getStatus() != 0) {
                // 1 exception is thrown
                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) {
...
    }
 
    // list of exception error messages
    private List<String> getMessagesForException(Exception exception) {
        // retrieve the list of exception error messages
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            // the message is retrieved only if it is !=null and not blank
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // next cause
            cause = cause.getCause();
        }
        return erreurs;
    }
}
 

نوع [Response] المستخدم في السطر 37 هو استجابة خادم الويب/JSON الموضحة في القسم 14.7؛


المهمة: وفقًا للفقرة 13.6.3.7، أكمل فئة [ElectionsMetier


15.7. اختبار JUnit

لنعد إلى بنية العميل/الخادم قيد الإنشاء حاليًا:

تتواصل طبقة [JUnit] [1] مع طبقة [Business] [5] في الخادم عبر الطبقات [2–4]. ومن خلال ضمان أن تكون لطبقتي [Business] [2] و[5] نفس الواجهة، نجعل الطبقات [2–4] شفافة. ويبدو أن الطبقة [1] تتواصل مباشرة مع الطبقة [5]. النقطة المثيرة للاهتمام هي أنه في [1] سنتمكن من استخدام اختبار JUnit الذي تم استخدامه لاختبار طبقة [الأعمال] [5].

  

المهمة: قم بتشغيل اختبار JUnit الخاص بالمشروع للتحقق من صحة تنفيذك لكل من الخادم والعميل.


15.8. تنفيذ طبقة [UI]

لنعد إلى البنية التي نريد بناءها:

الآن بعد أن تم بناء واختبار طبقة [الأعمال] [2]، يمكننا بناء طبقة [واجهة المستخدم] [1].

نظرًا لأن واجهة [IElectionsMetier] الخاصة بطبقة [الأعمال] مطابقة لتلك الموجودة في المشروع الموصوف في الفقرة 8، يمكننا [3] نسخ مشروع طبقة [واجهة المستخدم] من الفقرة 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 context retrieval
    protected AnnotationConfigApplicationContext ctx;
 
    public void run() {
        // instantiation layer [ui]
        IElectionsUI electionsUI = null;
        try {
            // spring context retrieval
            ctx = new AnnotationConfigApplicationContext(UiConfig.class);
            // ui] layer recovery
            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: يستورد الفاصوليا من طبقة [business
  • السطر 7: نشير إلى وجود مكونات Spring في حزمة [elections.client.ui

المهمة: تحقق من أن إصدارات وحدة التحكم و Swing من طبقة [ui] تعمل.