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] تعمل.















