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] بتكوين Spring للطبقة [métier]. ورمزها هو التالي:


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);
    }

    // مُستردات ومُعيّنات
    ...
}

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) {
                // يتم إلقاء استثناء واحد
                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: نوضح أن هناك حبوب Spring في الحزمة [elections.client.ui

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