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] يعملان.















