18. عميل مبرمج لخدمة الويب / JSON
الآن بعد أن أصبحت قاعدة البيانات [dbproduitscategories] متاحة على الويب، سنقوم بكتابة تطبيق يستخدمها. عندئذٍ سيكون لدينا بنية العميل/الخادم التالية:
![]() |
سيتكون تطبيق العميل من ثلاث طبقات:
- طبقة [HTTP Client] [3] للتواصل مع تطبيق الويب /jSON الذي يعرض قاعدة البيانات؛
- طبقة [DAO] [2] ستقدم نفس واجهة طبقة [DAO] [4]؛
- طبقة اختبار JUnit [1] للتحقق من أن العميل والخادم يعملان بشكل صحيح؛
18.1. مشروع Eclipse
مشروع Eclipse الخاص بالعميل هو كما يلي:
![]() |
![]() | ![]() | ![]() |
![]() | ![]() |
- تحتوي حزمة [spring.webjson.client.config] على تكوين Spring لطبقة [DAO]؛
- تحتوي الحزمة [spring.webjson.client.dao] على تنفيذ طبقة [DAO]؛
- تحتوي حزمة [spring.webjson.client.entities] على الكائنات التي يتم تبادلها مع خدمة الويب / JSON. نحن على دراية بها جميعًا؛
- تحتوي حزمة [spring.webjson.client.infrastructure] على فئات الاستثناءات المستخدمة في المشروع. ونحن نعرفها جميعًا؛
18.2. تكوين Maven للمشروع
المشروع هو مشروع Maven تم تكوينه بواسطة ملف [pom.xml] التالي:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dvp.spring.database</groupId>
<artifactId>spring-webjson-client-generic</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>Client console du serveur web / jSON</description>
<name>spring-webjson-client-generic</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- jSON library used by Spring -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- component used by Spring RestTemplate -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
<!-- log library -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- plugins -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- الأسطر 16–20: مشروع Maven الأصلي [spring-boot-starter-parent]، الذي يسمح لنا بتعريف عدد من التبعيات دون تحديد إصداراتها، حيث يتم تعريفها في المشروع الأصلي؛
- الأسطر 24–27: على الرغم من أننا لا نكتب تطبيق ويب، فإننا نحتاج إلى التبعية [spring-web]، التي تتضمن فئة [RestTemplate] التي تتيح التفاعل السهل مع تطبيق ويب أو JSON؛
- الأسطر 29–36: مكتبة JSON؛
- الأسطر 38–41: تبعية تسمح لنا بتعيين مهلة لطلبات HTTP الخاصة بالعميل. المهلة هي أقصى وقت انتظار لاستجابة الخادم. بعد هذا الوقت، يشير العميل إلى خطأ المهلة عن طريق إلقاء استثناء؛
- الأسطر 43-48: مكتبة Google Guava؛
- الأسطر 50-53: مكتبة التسجيل؛
- الأسطر 54–64: التبعية لاختبارات JUnit. وهي تتضمن مكتبة JUnit 4 المطلوبة للاختبار. تحتوي هذه التبعيات على السمة [<scope>test</scope>]، مما يشير إلى أنها مطلوبة فقط لمرحلة الاختبار. ولا يتم تضمينها في أرشيف المشروع النهائي؛
18.3. تكوين Spring
![]() |
تتولى فئة [AppConfig] تكوين Spring لعميل HTTP. وفيما يلي شفرة البرمجة الخاصة بها:
package spring.webjson.client.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@Configuration
@ComponentScan({ "spring.webjson.client.dao" })
public class AppConfig {
// constants
static private final int TIMEOUT = 1000;
static private final String URL_WEBJSON = "http://localhost:8081";
// filters jSON
@Bean
public ObjectMapper jsonMapper(RestTemplate restTemplate) {
return ((MappingJackson2HttpMessageConverter) (restTemplate.getMessageConverters().get(0))).getObjectMapper();
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperShortCategorie(RestTemplate restTemplate) {
ObjectMapper jsonMapper = jsonMapper(restTemplate);
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongCategorie(RestTemplate restTemplate) {
ObjectMapper jsonMapper = jsonMapper(restTemplate);
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperShortProduit(RestTemplate restTemplate) {
ObjectMapper jsonMapper = jsonMapper(restTemplate);
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongProduit(RestTemplate restTemplate) {
ObjectMapper jsonMapper = jsonMapper(restTemplate);
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
return jsonMapper;
}
@Bean
public RestTemplate restTemplate(int timeout) {
// creation of the RestTemplate component
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// converter jSON
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
messageConverters.add(new MappingJackson2HttpMessageConverter());
restTemplate.setMessageConverters(messageConverters);
// exchange timeout
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
// result
return restTemplate;
}
@Bean
public int timeout() {
return TIMEOUT;
}
@Bean
public String urlWebJson() {
return URL_WEBJSON;
}
}
- السطر 20: الفئة هي فئة تكوين Spring؛
- السطر 21: يمكن العثور على مكونات Spring الأخرى في حزمة [spring.webjson.client.dao]؛
- السطر 25: تم تعيين مهلة انتظار مدتها ثانية واحدة (1000 مللي ثانية)؛
- الأسطر 88–91: الكائن الذي يُرجع هذه القيمة؛
- السطر 26: عنوان URL لخدمة الويب / JSON؛
- الأسطر 93-96: الحبة التي تُرجع هذه القيمة؛
- الأسطر 72-86: تكوين فئة [RestTemplate] التي تتولى الاتصال بخدمة الويب/JSON. عندما لا يكون التكوين مطلوبًا، يمكن إنشاء مثيل لها في الكود باستخدام [new RestTemplate()] البسيط. هنا، نريد تعيين مهلة الاتصال بخدمة الويب/JSON. يتم تمرير bean [timeout] في السطر 89 كمعلمة إلى طريقة [restTemplate] في السطر 73؛
- السطر 75: المكون [HttpComponentsClientHttpRequestFactory] هو الذي يسمح لنا بتعيين مهلة الاتصال (السطور 82-83)؛
- السطر 76: يتم إنشاء فئة [RestTemplate] باستخدام هذا المكون. نظرًا لأنها تعتمد على هذا المكون للتواصل مع خدمة الويب / JSON، فإن التبادلات ستخضع بالفعل لحد زمني؛
- الأسطر 78-80: نربط محول JSON بفئة [RestTemplate]. لقد ناقشنا هذا بالفعل عند دراسة خدمة الويب. يتبادل العميل والخادم أسطر من النص. يقوم المحول بتسلسل كائن إلى نص وإلغاء تسلسل النص مرة أخرى إلى كائن. يمكن أن يكون هناك عدة محولات مرتبطة بفئة [RestTemplate]، ويعتمد اختيار المحول في أي وقت معين على رؤوس HTTP المرسلة من الخادم. هنا، لدينا محول JSON فقط لأن أسطر النص المتبادلة هي JSON؛
- السطران 82-83: يتم تعيين مهلات التبادل؛
- الأسطر 28–70: تعريف مرشحات JSON. وهي نفس المرشحات الموجودة على الخادم والموضحة في القسم 17.3.2.1؛
- الأسطر 29–32: حبة [jsonMapper] هي أداة تعيين JSON لـ [MappingJackson2HttpMessageConverter] التي قمنا بربطها بفئة [RestTemplate]. نحتاج إلى هذا في تعريف مرشحات JSON؛
- الأسطر 34–41: حبة تحدد مرشح JSON [الفئة بدون منتجاتها]. تأخذ طريقة [jsonMapperShortCategory] حبة [RestTemplate] المحددة في السطر 73 كمعلمة؛
- السطر 37: نستدعي طريقة [jsonMapper] من السطر 30 لاسترداد مخطط JSON؛
- الأسطر 38-39: نضبط المرشح ليعرض فئة بدون منتجاتها؛
- السطر 40: يتم إرجاع مخطط JSON كما تم تكوينه؛
- الأسطر 42-51: مرشح JSON لاسترداد فئة مع منتجاتها؛
- الأسطر 53-60: مرشح JSON لاسترداد منتج بدون فئته؛
- الأسطر 62-70: مرشح JSON لاسترداد منتج مع فئته؛
ستكون جميع هذه الفاصوليا متاحة لرمز طبقة [DAO] وكذلك لاختبارات JUnit.
18.4. تنفيذ عميل HTTP
![]() |
أعلاه توجد طبقة [HTTP Client] التي تتواصل مع خدمة الويب التي أنشأناها للتو. سنقوم الآن بفحصها.
![]() |
تتولى فئة [Client] إدارة الاتصال بخدمة الويب / JSON. وهي تنفذ واجهة [IClient] التالية:
package spring.webjson.client.dao;
import org.springframework.http.HttpMethod;
public interface IClient {
public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body);
}
تحتوي الواجهة على طريقة واحدة فقط [getResponse]:
- السطر 6: طريقة [getResponse] هي طريقة عامة معلمة بنوعين:
- [T1]: هو نوع الاستجابة المتوقع من الخادم في [Response<T1>]، على سبيل المثال [List<Category>]،
- [T2]: هو نوع معلمة JSON التي يتم إرسالها بواسطة عمليات POST، على سبيل المثال [List<Product>]؛
- السطر 6: تُرجع طريقة [getResponse] نتيجة من النوع T1، على سبيل المثال [List<Category>]؛
- السطر 6: معلمات [getResponse] هي كما يلي:
- [String url]: عنوان URL المراد الاستعلام عنه؛
- [HttpMethod method]: طريقة HTTP للطلب، GET أو POST حسب الاقتضاء،
- [int errStatus]: رمز الخطأ الذي سيتم استخدامه في فئة [DaoException]، في حالة حدوث خطأ أثناء الاتصال بالخادم،
- [T2 body]: القيمة المراد إرسالها في حالة إجراء طلب POST؛
تنفذ فئة [Client] واجهة [IClient] على النحو التالي:
package spring.webjson.client.dao;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import spring.webjson.client.infrastructure.DaoException;
@Component
public class Client implements IClient {
// injections
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// local
private String simpleClassName = getClass().getSimpleName();
// generic request
@Override
public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
...
}
// list of exception error messages
protected List<String> getMessagesForException(Exception exception) {
...
}
}
- السطر 18: فئة [Client] هي مكون Spring وبالتالي يمكن حقنها في مكونات Spring الأخرى؛
- السطران 22-23: حقن حبة [RestTemplate] المحددة في [AppConfig] (انظر القسم 18.3)، والتي تتولى الاتصال بالخادم؛
- السطران 24-25: حقن عنوان URL لخدمة الويب / JSON المحدد في [AppConfig] (انظر القسم 18.3)؛
- الأسطر 37–39: الطريقة الخاصة [getMessagesForException] هي طريقة مساعدة تُستخدم لاسترداد قائمة رسائل الخطأ الموجودة في استثناء. وقد صادفناها عدة مرات؛
لنواصل:
// generic request
@Override
public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
// the server response
ResponseEntity<Response<T1>> response;
try {
// prepare the query
RequestEntity<?> request = null;
if (method == HttpMethod.GET) {
request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
.accept(MediaType.APPLICATION_JSON).build();
}
if (method == HttpMethod.POST) {
request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(body);
}
// execute the query
response = restTemplate.exchange(request, new ParameterizedTypeReference<Response<T1>>() {
});
} catch (Exception e) {
// encapsulate the exception
throw new DaoException(errStatus, e, simpleClassName);
}
...
}
- السطر 18: العبارة التي ترسل الطلب إلى الخادم وتستقبل استجابته. يوفر مكون [RestTemplate] عددًا كبيرًا من الطرق للتفاعل مع الخادم، لكن طريقة [exchange] هي الوحيدة التي تقبل المعلمات العامة. ولهذا السبب تم اختيارها. تحدد المعلمة الثانية نوع الاستجابة المتوقعة. أما المعلمة الأولى فهي طلب [RequestEntity] (السطر 8). نتيجة طريقة [exchange] هي من النوع [ResponseEntity<Response<T1>>] (السطر 5). يغلف النوع [ResponseEntity] الرد الكامل للخادم، بما في ذلك رؤوس HTTP والمستند المرسل من الخادم. وبالمثل، يغلف النوع [RequestEntity] الطلب الكامل للعميل، بما في ذلك رؤوس HTTP وأي بيانات مرسلة؛
- الأسطر 8-16: نحتاج إلى إنشاء طلب [RequestEntity]. ويختلف ذلك اعتمادًا على ما إذا كنا نستخدم طلب GET أو POST؛
- السطر 10: طلب GET. توفر فئة [RequestEntity] طرقًا ثابتة لإنشاء طلبات GET وPOST وHEAD وغيرها. تتيح لك طريقة [RequestEntity.get] إنشاء طلب GET من خلال ربط الطرق المختلفة التي تبنيه:
- تأخذ الطريقة [RequestEntity.get] عنوان URL الهدف كمعلمة في شكل مثيل URI،
- تسمح لك الطريقة [accept] بتحديد عناصر رأس HTTP [Accept]. هنا، نحدد أننا نقبل النوع [application/json] الذي سيرسله الخادم؛
- تستخدم طريقة [build] هذه المعلومات لإنشاء نوع [RequestEntity] للطلب؛
- السطر 14: طلب POST. تتيح لك طريقة [RequestEntity.post] إنشاء طلب POST عن طريق ربط الطرق المختلفة التي تبنيه:
- تأخذ طريقة [RequestEntity.post] عنوان URL الهدف كمعلمة في شكل مثيل URI،
- تحدد طريقة [header] رأس HTTP. هنا، نرسل رأس [Content-Type: application/json] إلى الخادم للإشارة إلى أن البيانات المرسلة ستصل في شكل سلسلة JSON؛
- تسمح لنا طريقة [accept] بالإشارة إلى أننا نقبل النوع [application/json] الذي سيرسله الخادم؛
- تقوم طريقة [body] بتعيين القيمة المرسلة. هذه هي المعلمة الرابعة لطريقة [getResponse] العامة (السطر 1)؛
- الأسطر 20-23: في حالة حدوث خطأ في الاتصال بالخادم، يتم إلقاء استثناء [DaoException] مع تعيين رمز الخطأ إلى المعلمة [errStatus] التي تم تمريرها كمعلمة ثالثة إلى الطريقة العامة [getResponse] (السطر 3)؛
تستمر طريقة [getResponse] على النحو التالي:
// generic request
@Override
public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
...
// retrieve the body of the reply
Response<T1> entity = response.getBody();
int status = entity.getStatus();
// server-side errors?
if (status != 0) {
// create an exception
throw new DaoException(status, new RuntimeException(entity.getException()), simpleClassName);
} else {
// it's good
return entity.getBody();
}
}
- السطر 4: لقد تلقينا الاستجابة من الخادم. وهي من النوع [ResponseEntity<Response<T1>>] (السطر 5 من مثال الكود السابق) حيث فئة [Response] هي الفئة المستخدمة بالفعل على جانب الخادم:
package spring.webjson.client.dao;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// the possible exception
private String exception;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, String exception, T body) {
this.status = status;
this.exception = exception;
this.body = body;
}
// getters and setters
...
}
لنعد إلى طريقة [getResponse]:
- السطر 6: نسترد الكائن [Response<T1>] المُغلف في الاستجابة. يحتوي هذا النوع على الحقول [int status, String exception, T1 body]؛
- السطر 7: نسترد [status] من الاستجابة، وهو رمز خطأ؛
- الأسطر 9-12: إذا كان هناك خطأ، فإننا نلقي استثناءً يحتوي على معلومتين [status, exception] من استجابة الخادم؛
- السطر 14: خلاف ذلك، نُرجع النوع [T1] الموجود في الاستجابة [Response<T1>]؛
فئة [Client] عامة. يمكن استخدامها لأي عميل ويب/JSON.
18.5. تنفيذ طبقة [Dao]
![]() |
![]() |
18.5.1. فئة [AbstractDao]
طبقة [DAO] من جانب العميل لها نفس واجهة طبقة [DAO] من جانب الخادم (انظر القسم 4.7):
package spring.webjson.client.dao;
import java.util.List;
import spring.webjson.client.entities.AbstractCoreEntity;
public interface IDao<T extends AbstractCoreEntity> {
// list of all T entities
public List<T> getAllShortEntities();
public List<T> getAllLongEntities();
// special entities - short version
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
public List<T> getShortEntitiesByName(Iterable<String> names);
public List<T> getShortEntitiesByName(String... names);
// special entities - long version
public List<T> getLongEntitiesById(Iterable<Long> ids);
public List<T> getLongEntitiesById(Long... ids);
public List<T> getLongEntitiesByName(Iterable<String> names);
public List<T> getLongEntitiesByName(String... names);
// update of several entities
public List<T> saveEntities(Iterable<T> entities);
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
// delete all entities
public void deleteAllEntities();
// deletion of multiple entities
public void deleteEntitiesById(Iterable<Long> ids);
public void deleteEntitiesById(Long... ids);
public void deleteEntitiesByName(Iterable<String> names);
public void deleteEntitiesByName(String... names);
public void deleteEntitiesByEntity(Iterable<T> entities);
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}
تنفذ فئة [AbstractDao] واجهة [IDao]. وهي مماثلة للفئة التي تحمل الاسم نفسه على جانب الخادم (انظر القسم 4.8). وهي تعمل كفئة أصلية لفئتي [DaoCategorie] و[DaoProduit]. وهي ليست متطابقة لسببين:
- على جانب الخادم، تدير فئة [AbstractDao] معلومة واحدة:
// injections
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
وهو ما لا نحتاج إليه هنا.
- على جانب الخادم، تستخدم فئة [AbstractDao] تعليقات [@Transactional] لتغليف كل طريقة داخل معاملة. على جانب العميل، لا توجد قاعدة بيانات لإدارتها. وبالتالي، تختفي هذه التعليقات؛
تقوم فئة [AbstractDao] ببساطة بالتحقق من صحة معلمات الاستدعاء لطرق واجهة [IDao] قبل تفويض الاستدعاء إلى الفئات الفرعية:
package spring.webjson.client.dao;
import java.util.ArrayList;
import java.util.List;
import spring.webjson.client.entities.AbstractCoreEntity;
import spring.webjson.client.infrastructure.MyIllegalArgumentException;
import com.google.common.collect.Lists;
public abstract class AbstractDao<T1 extends AbstractCoreEntity> implements IDao<T1> {
// local
protected String simpleClassName = getClass().getSimpleName();
@Override
public List<T1> getShortEntitiesById(Iterable<Long> ids) {
// argument validity
List<T1> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// result
return getShortEntitiesById(Lists.newArrayList(ids));
}
@Override
public List<T1> getShortEntitiesById(Long... ids) {
// argument validity
List<T1> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// result
return getShortEntitiesById(Lists.newArrayList(ids));
}
...
@Override
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T1... entities) {
...
}
// méthodes privées ----------------------------------------------
private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T3> elements) {
// elements null ?
if (elements == null) {
throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),
simpleClassName);
}
// empty elements?
if (!elements.iterator().hasNext()) {
if (checkEmpty) {
throw new MyIllegalArgumentException(223, new RuntimeException("l'argument ne peut être une liste vide"),simpleClassName);
} else {
return new ArrayList<T1>();
}
}
// default result
return null;
}
@SuppressWarnings("unchecked")
private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, T3... elements) {
// elements null ?
if (elements == null) {
throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),simpleClassName);
}
// empty elements?
if (elements.length == 0) {
if (checkEmpty) {
throw new MyIllegalArgumentException(223, new RuntimeException("L'argument ne peut être une liste vide"),
simpleClassName);
} else {
return new ArrayList<T1>();
}
}
// default result
return null;
}
// méthodes protégées ----------------------------------------------
abstract protected List<T1> getShortEntitiesById(List<Long> ids);
abstract protected List<T1> getShortEntitiesByName(List<String> names);
abstract protected List<T1> getLongEntitiesById(List<Long> ids);
abstract protected List<T1> getLongEntitiesByName(List<String> names);
abstract protected List<T1> saveEntities(List<T1> entities);
abstract protected void deleteEntitiesById(List<Long> ids);
abstract protected void deleteEntitiesByName(List<String> names);
}
18.5.2. فئة [DaoCategorie]
![]() |
فئة [DaoCategorie] هي كما يلي:
package spring.webjson.client.dao;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import spring.webjson.client.entities.Categorie;
import spring.webjson.client.entities.CoreCategorie;
import spring.webjson.client.entities.CoreProduit;
import spring.webjson.client.entities.Produit;
import spring.webjson.client.infrastructure.DaoException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@Component
public class DaoCategorie extends AbstractDao<Categorie> {
@Autowired
private ApplicationContext context;
@Autowired
private IClient client;
...
}
- السطر 19: فئة [DaoClient] هي مكون Spring يمكن حقن مكونات Spring أخرى فيه؛
- السطر 20: تمتد فئة [DaoClient] إلى فئة [AbstractDao<Category>] التي رأيناها للتو، وبالتالي فإنها تنفذ واجهة [IDao<Category>]؛
- السطران 22-23: نقوم بإدخال سياق Spring للوصول إلى مكوناته؛
- السطران 24–25: نقوم بإدخال عميل HTTP الذي أنشأناه للتو؛
تتبع جميع تطبيقات الطرق المختلفة لواجهة [DaoCategorie] نفس النمط. سنقدم ثلاث طرق، واحدة تعتمد على عملية [GET]، والأخريان على عملية [POST].
18.5.2.1. طريقة [getAllLongEntities]
تُرجع طريقة [getAllLongEntities] النسخة الطويلة لجميع الفئات الموجودة في قاعدة البيانات:
@Override
public List<Categorie> getAllLongEntities() {
try {
// filters jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// get all categories
Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
// the List<Categorie> category list
List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<Categorie>>() {
});
// redo the product --> category link
return linkCategorieWithProduits(categories);
} catch (DaoException e1) {
throw e1;
} catch (Exception e2) {
throw new DaoException(233, e2, simpleClassName);
}
}
- السطر 2: تُرجع الطريقة قائمة الفئات في صيغتها الطويلة؛
- السطر 5: أداة تعيين JSON التي ستقوم بتسلسل القيمة المنشورة (لا يوجد واحدة) وإلغاء تسلسل الاستجابة التي تعيدها فئة [Client] (الفئات في صيغتها الطويلة)؛
- السطر 7: نستدعي طريقة [getResponse] لفئة [Client]. تتولى هذه الطريقة الاتصال بخدمة الويب / JSON. معلماتها هي كما يلي:
- عنوان URL للخدمة التي يتم الاستعلام عنها [/getAllLongCategories]؛
- طريقة [GET] المطلوب استخدامها؛
- رمز الخطأ الذي سيتم استخدامه في حالة حدوث خطأ (232)؛
- القيمة المرسلة. هنا، لا توجد أي قيمة؛
- السطر 7: في التعبير [client.<List<Category>, Void>]، نحدد المعلمات الفعلية للأنواع العامة [T1، T2] لطريقة [getResponse]. تذكر أن [T1] هو نوع الاستجابة المتوقعة و[T2] هو نوع القيمة المنشورة. هنا، نتوقع نتيجة من النوع [List<Category>] ولا توجد قيمة منشورة [Void]؛
- السطر 7: يتم تخزين النتيجة التي ترجعها طريقة [getResponse] في كائن من النوع [Object]. هذا غريب بعض الشيء لأننا نتوقع نوع [List<Category>]. ويرجع ذلك إلى أن طريقة [getResponse]، التي تعمل مع الأنواع العامة [T1, T2]، ترجع دائمًا نوع [java.util.LinkedHashMap]، والذي يجب معالجته بعد ذلك لإرجاع النوع الصحيح؛
- السطر 9: نُرجع قائمة الفئات. للقيام بذلك، نقوم بتسلسل كائن [map] [mapper.writeValueAsString(map)] إلى سلسلة JSON، ثم نقوم بإلغاء تسلسلها مرة أخرى إلى نوع [List<Category>]؛
- السطر 13: لقد تلقينا قائمة بالفئات، وقد تحتوي بعضها على منتجات. نتلقى النسخة المختصرة من هذه المنتجات. لذلك، عند إزالة تسلسلها، يتم تعيين حقل [category] في كائنات [Product] التي تم إنشاؤها إلى null. تعيد طريقة [linkCategoryWithProducts] إنشاء الارتباط بين [Product] و [Category] الخاصة به؛
- السطور 14–15: نلتقط [DaoException] التي قد تكون طريقة [getResponse] قد أطلقتها ونعيد إطلاقها على الفور. هذا السلوك غير المعتاد يرجع إلى أنه إذا لم نفعل ذلك، فسيتم التقاط [DaoException] بواسطة السطور 16–18، ونحن لا نريد ذلك؛
- الأسطر 16–18: نلتقط جميع الاستثناءات الأخرى لتغليفها في نوع [DaoException]. تذكر أن طبقة [DAO] يجب أن ترمي هذا النوع من الاستثناءات فقط؛
طريقة [linkCategorieWithProduits]، التي تعيد إنشاء الروابط بين كيانات [Product] وكيانات [Category]، هي كما يلي:
private List<Categorie> linkCategorieWithProduits(List<Categorie> categories) {
for (Categorie categorie : categories) {
List<Produit> produits = categorie.getProduits();
if (produits != null) {
for (Produit produit : produits) {
produit.setCategorie(categorie);
}
}
}
return categories;
}
18.5.2.2. إدارة مرشحات JSON
دعونا نعيد النظر في معالجة مرشحات JSON في الطريقة [getAllLongEntities] السابقة:
@Override
public List<Categorie> getAllLongEntities() {
try {
// filters jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// get all categories
Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
// the List<Categorie> category list
List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<Categorie>>() {
});
- السطر 5: نسترد مُعِد خرائط JSON من سياق Spring يمكنه التعامل مع الإصدارات الطويلة للفئات. دعونا نراجع تعريف مُعِد الخرائط هذا في تكوين Spring [AppConfig]:
// filters jSON
@Bean
public ObjectMapper jsonMapper(RestTemplate restTemplate) {
return ((MappingJackson2HttpMessageConverter) (restTemplate.getMessageConverters().get(0))).getObjectMapper();
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongCategorie(RestTemplate restTemplate) {
ObjectMapper jsonMapper = jsonMapper(restTemplate);
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
public RestTemplate restTemplate(int timeout) {
...
}
- البيان [jsonMapperLongCategorie] الذي تطلبه طريقة [getAlllongEntities] هو البيان الموجود في الأسطر 7–15؛
- السطر 10: يتم توفير أداة التعيين بواسطة طريقة [jsonMapper] في الأسطر 2-5. يمكننا أن نرى أن أداة التعيين JSON هذه تنتمي إلى كائن [RestTemplate]، الذي يدير تبادلات HTTP بين العميل والخادم. تُستخدم أداة التعيين هذه افتراضيًا من أجل:
- تسلسل القيمة المرسلة إلى الخادم؛
- إلغاء تسلسل الاستجابة التي يعيدها الخادم؛
لنعد إلى كود [getAllLongEntities]:
// filters jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// get all categories
Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
// the List<Categorie> category list
List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<Categorie>>() {
});
// redo the product --> category link
return linkCategorieWithProduits(categories);
- السطر 2: نسترد أداة التعيين [jsonMapperLongCategorie] من سياق Spring؛
- السطر 4: يتم تنفيذ طريقة [getResponse]. وهذا يتضمن:
- التسلسل التلقائي للقيمة المنشورة (لا توجد قيمة هنا)؛
- التسلسل التلقائي للاستجابة المستلمة، وهي هنا من نوع List<Category>. ويرجع ذلك إلى أن الكيان [Category] يحتوي على مرشح JSON [jsonFilterCategory]، والذي كان لا بد من معالجته. وهذا هو سبب السطر 2؛
- السطر 6: تخضع النتيجة لعملية تسلسل/إلغاء تسلسل ثانية باستخدام نفس أداة التعيين هذه لاسترداد النوع List<Category>. السطر 4: النوع الذي يتم إرجاعه بواسطة [getResponse] هو نوع [Object]؛
في الطرق التالية، لاحظ أن مُعِد التعيين JSON المطلوب من سياق Spring يُستخدم لكل من القيمة المرسلة (التسلسل) والقيمة المستلمة (إلغاء التسلسل). إذا كانت إحدى القيمتين أو كلتاهما تحتويان على مرشح JSON، فيجب تهيئتهما. وبالتالي، يمكن أن يحتوي مُعِد التعيين على ما يصل إلى مرشحين مُهيئين. وفي ما يلي، لا يحدث هذا أبدًا. إما أن القيمة المنشورة لا تحتوي على مرشح (List<Long>، List<String>)، أو أن القيمة المستلمة لا تحتوي على مرشح (List<CoreCategory>، List<CoreProduct>). الكيانات التي تحتوي على مرشح JSON هي [Category] و [Product] فقط.
18.5.2.3. طريقة [getShortEntitiesById]
تُرجع الطريقة [getShortEntitiesById] النسخ المختصرة للفئات التي تتلقى مفاتيحها الأساسية كمعلمات:
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
try {
// filters jSON
ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
// get a category without its products
Object map = client.<List<Categorie>, List<Long>> getResponse("/getShortCategoriesById", HttpMethod.POST, 204, ids);
// the category
return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
});
} catch (DaoException e1) {
throw e1;
} catch (Exception e2) {
throw new DaoException(223, e2, simpleClassName);
}
}
- السطر 5: أداة تعيين JSON التي ستقوم بتسلسل القيمة المنشورة (قائمة بالمفاتيح الأساسية) وإلغاء تسلسل الاستجابة التي تعيدها فئة [Client] (الفئات في صيغتها المختصرة). لن يكون للمرشح المختار أي تأثير على القيمة المنشورة نظرًا لعدم وجود مرشح للعناصر الموجودة في القائمة المنشورة؛
- السطر 7: نستدعي طريقة [getResponse] للفئة الأصلية. تتولى هذه الطريقة الاتصال بخدمة الويب / JSON. معلماتها هي كما يلي:
- عنوان URL للخدمة التي يتم الاستعلام عنها [/getShortCategoriesById]؛
- طريقة [POST] المراد استخدامها؛
- رمز الخطأ الذي سيتم استخدامه في حالة حدوث خطأ (204)؛
- القيمة المنشورة. هنا، هي قائمة بالمفاتيح الأساسية؛
- السطر 7: في التعبير [client.<List<Category>, List<Long>>]، نحدد المعلمات الفعلية للأنواع العامة [T1, T2] لطريقة [getResponse]. تذكر أن [T1] هو نوع الاستجابة المتوقعة و[T2] هو نوع القيمة المنشورة. هنا، نتوقع نتيجة من النوع [List<Category>] والقيمة المنشورة هي قائمة بالمفاتيح الأساسية من النوع [List<Long>]؛
- السطر 7: يتم تخزين النتيجة التي ترجعها طريقة [getResponse] في نوع [Object]؛
- السطر 9: يتم إرجاع قائمة الفئات. للقيام بذلك، يتم تسلسل كائن [map] [mapper.writeValueAsString(map)] إلى سلسلة JSON، والتي يتم بعد ذلك فك تسلسلها إلى نوع [List<Category>]؛
18.5.2.4. طريقة [saveEntities]
تقوم طريقة [saveEntities] بحفظ الفئات في قاعدة البيانات. وفيما يلي كودها:
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
try {
// filters jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// add categories
Object map = client.<List<CoreCategorie>, List<Categorie>> getResponse("/saveCategories", HttpMethod.POST, 200,
entities);
// list of added core categories
List<CoreCategorie> coreCategories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<CoreCategorie>>() {
});
// categories are updated with the information received
for (int i = 0; i < entities.size(); i++) {
Categorie categorie = entities.get(i);
CoreCategorie coreCategorie = coreCategories.get(i);
categorie.setId(coreCategorie.getId());
List<Produit> produits = categorie.getProduits();
if (produits != null) {
List<CoreProduit> coreProduits = coreCategorie.getCoreProduits();
for (int j = 0; j < produits.size(); j++) {
Produit produit = produits.get(j);
produit.setId(coreProduits.get(j).getId());
produit.setIdCategorie(categorie.getId());
produit.setCategorie(categorie);
}
}
}
return entities;
} catch (DaoException e1) {
throw e1;
} catch (Exception e2) {
throw new DaoException(220, e2, simpleClassName);
}
}
- السطر 2: تُستخدم طريقة [saveEntities] لتخزين الفئات التي تم تمريرها كمعلمات في قاعدة البيانات. وتُرجع هذه الفئات نفسها مع إضافة مفاتيحها الأساسية. إذا تم تمرير الفئات مع المنتجات، يتم تخزين هذه المنتجات أيضًا؛
- السطر 5: مُعِدّ خرائط JSON الذي سيقوم بتسلسل القيمة المنشورة (قائمة بالفئات في نسخها الطويلة) وإلغاء تسلسل الاستجابة التي تعيدها فئة [Client] (كائنات [CoreCategory]). لن يكون للمرشح المختار أي تأثير على النتيجة لأن العناصر في القائمة التي تم استلامها كاستجابة لا يتم ترشيحها؛
- السطر 7: نستدعي طريقة [getResponse] الخاصة بالوالد للتعامل مع الاتصال بخدمة الويب / JSON؛
- المعلمة الأولى هي عنوان URL [/saveCategories]؛
- المعلمة الثانية هي طريقة HTTP المطلوب استخدامها، وهي في هذه الحالة [POST]؛
- المعلمة الثالثة هي رمز الخطأ الذي سيتم استخدامه في حالة حدوث خطأ (200)؛
- المعلمة الأخيرة هي القيمة المرسلة، وهي هنا قائمة الفئات المراد الاحتفاظ بها؛
- السطر 7: المعلمات العامة [T1, T2] لطريقة [getResponse] هي هنا [List<CoreCategory>, List<Category>]. النوع الأول هو نوع الاستجابة المتوقعة، والثاني هو نوع القيمة المرسلة؛
- السطر 7: نقوم بتخزين الاستجابة التي تم الحصول عليها في نوع [Object]؛
- السطر 9: نعيد بناء الاستجابة من النوع [List<CoreCategory>]. الاستجابة المراد إرجاعها هي من النوع [List<Category>] (السطر 2) وليست [List<CoreCategory>]. الاستجابة المستلمة هي قائمة بالمفاتيح الأساسية للفئات والمنتجات التي تم الاحتفاظ بها؛
- الأسطر 14-28: يتم تعيين المفاتيح الأساسية المستلمة إلى الفئات والمنتجات (الأسطر 17 و23 و24). بالإضافة إلى ذلك، يتم إعادة بناء العلاقات [Product] → [Category] (الأسطر 24-25)؛
تتبع جميع الطرق الأخرى نفس النمط.
18.6. اختبار JUnit
لنعد إلى بنية العميل/الخادم قيد التطوير حاليًا:
![]() |
لقد أنشأنا طبقة [DAO] [2] بنفس واجهة طبقة [DAO] [4]. ولذلك، يمكننا استخدام اختبارات JUnit التي استُخدمت لاختبار طبقة [DAO] [4] لاختبار طبقة [DAO] [2]:
![]() |
يتم إجراء هذه الاختبارات الثلاثة باستخدام تكوينات الاختبار التالية:
![]() | ![]() |
![]() |
فيما يلي نتائج الاختبارات الثلاثة:
![]() |
![]() |
- في [1]، اختبار [JUnitTestCheckArguments]؛
- في [2]، اختبار [JUnitTestDao]؛
- في [3]، اختبار [JUnitTestPushTheLimits] الذي تم تنفيذه على جانب العميل (مشروع [spring-webjson-client-generic])؛
- في [3]، اختبار [JUnitTestPushTheLimits] الذي تم تنفيذه على جانب الخادم (مشروع [spring-jdbc-generic-04]). نلاحظ أن طبقة الشبكة تسبب تباطؤًا ضئيلًا جدًا مقارنةً بالتباطؤ الناجم عن الوصول إلى نظام إدارة قواعد البيانات (DBMS)؛
18.7. تنفيذ خدمة الويب / JSON / JPA / Hibernate
سنقوم الآن بفحص البنية التالية:
![]() |
التعديل موجود في [1]. تعتمد طبقة [DAO] في الخادم على تطبيق JPA. سنستخدم أولاً تطبيق JPA / Hibernate.
18.7.1. مشروع Eclipse
في الوقت الحالي، المشاريع التي تم تحميلها في Eclipse هي كما يلي:
![]() |
استند مشروع [spring-webjson-server-jdbc-generic] إلى مشروع [spring-jdbc-generic-04]، الذي يقوم بتكوين طبقة DAO / JDBC للوصول إلى نظام إدارة قواعد البيانات MySQL. سننشئ مشروعًا جديدًا [spring-webjson-server-jpa-generic]، والذي سيعتمد على مشروع [spring-jpa-generic] الذي يقوم بتكوين طبقة DAO/JPA/JDBC للوصول إلى نظام إدارة قواعد البيانات MySQL. نعلم أنه في كلتا الحالتين، تقوم طبقة [DAO] بتنفيذ نفس واجهة [IDao]. وبالتالي، يظل كود طبقة [web] دون تغيير.
يمكننا إنشاء مشروع [spring-webjson-server-jpa-generic] عن طريق النسخ واللصق من مشروع [spring-webjson-server-jdbc-generic]:
![]() |
- في [1]، حدد مجلدًا تم إنشاؤه خصيصًا للمشروع الجديد؛
![]() |
هناك ثلاثة أنواع من التغييرات التي يجب إجراؤها. النوع الأول يقع في ملف تكوين Maven الخاص بالمشروع [pom.xml]:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dvp.spring.database</groupId>
<artifactId>spring-webjson-server-jpa-generic</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-webjson-server-jpa-generic</name>
<description>démo spring mvc</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- web layer -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- layer [DAO] -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>spring-jpa-generic</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<!-- plugins -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- السطر 5: قم بتغيير اسم عنصر Maven؛
- الأسطر 24–28: أصبحت التبعية الآن على مشروع [spring-jpa-generic] ولم تعد على [spring-jdbc-generic-04]؛
في النهاية، تكون التبعيات كما يلي:
![]() |
بمجرد الانتهاء من ذلك، نقوم بحل جميع مشكلات الاستيراد التي ظهرت في الفئات المختلفة. على سبيل المثال، لم تعد كيانات [Product، Category] موجودة في مشروع [spring-jdbc-generic-04] بل في مشروع [spring-jpa-generic]. يكفي الضغط على [Ctrl-Shift-O] في كود الفئة لإعادة إنشاء عمليات الاستيراد.
يجب إجراء التغيير الأخير في ملف التكوين [AppConfig]:
package spring.webjson.server.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@ComponentScan(basePackages = { "spring.webjson.server.service" })
@Import({ spring.data.config.AppConfig.class, WebConfig.class })
public class AppConfig {
}
- السطر 9: نقوم الآن باستيراد التكوين من مشروع [spring-jpa-generic] بدلاً من مشروع [spring-jdbc-generic-04]؛
هذا كل شيء — نحن جاهزون. نبدأ خدمة الويب باستخدام تكوين [spring-webjson-server-jpa-generic-hibernate-eclipselink]:
![]() | ![]() |
ثم نجري الاختبارات الثلاثة للعميل العام [spring-webjson-client-generic]:
![]() |
![]() |
- في [1]، اختبار [JUnitTestCheckArguments] (تكوين التشغيل [spring-webjson-client-generic-JUnitTestCheckArguments])؛
- في [2]، اختبار [JUnitTestDao] (تكوين التنفيذ [spring-webjson-client-generic-JUnitTestDao])؛
- في [3]، اختبار [JUnitTestPushTheLimits] الذي يتم تشغيله على جانب العميل (تكوين التشغيل [spring-webjson-client-generic-JUnitTestPushTheLimits])؛
- في [4]، اختبار [JUnitTestPushTheLimits] الذي تم تنفيذه على جانب الخادم (تكوين التشغيل [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink])؛
18.7.2. لماذا يعمل؟
إنه يعمل، ولكن عندما تنظر عن كثب إلى الكود، فإنك ستندهش من أنه يعمل. في حين أن طبقات [DAO] التي تم تنفيذها بواسطة مشروعي [spring-jdbc-generic-04] و [spring-jpa-generic] تقدم بالفعل نفس الواجهة، إلا أنها لا تتعامل مع نفس كيانات [Category] و [Product]: في مشروع [spring-jpa-generic]، تحتوي هذه الكيانات على حقل إضافي [EntityType entityType] يمكن أن يأخذ قيمتين محتملتين:
- EntityType.POJO: الكيان هو كائن عادي يمكن استخدام حقوله بحرية؛
- EntityType.PROXY: الكيان هو كائن PROXY يتم عرضه بواسطة طبقة [JPA]. في هذه الحالة، لا تعمل حقول معينة (في الواقع، أدوات الحصول على هذه الحقول) كالمعتاد، وقد تم وضع القواعد التالية:
- إذا كان [Category.entityType == EntityType.PROXY]، فلا يجب استخدام طريقة [getProducts]؛
- إذا كان [Product.entityType == EntityType.PROXY]، فلا يجب استخدام طريقة [getCategory]؛
ومع ذلك، فقد قمنا للتو بترحيل مشروع [spring-webjson-server-jdbc-generic] إلى [spring-webjson-server-jpa-generic] دون تعديل الكود. كيف يكون هذا ممكنًا؟
دعونا نفحص كود طريقة [saveCategories]:
@RequestMapping(value = "/saveCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Response<List<CoreCategorie>> saveCategories(HttpServletRequest request) {
...
// retrieve the posted value
String body = CharStreams.toString(request.getReader());
// we deserialize it
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
});
// we persist categories
categories = daoCategorie.saveEntities(categories);
...
}
- السطر 8: يتم إنشاء كائن List<Category> من سلسلة JSON:
- في القيمة المنشورة، لا تحتوي المنتجات على حقل [category]. في الواقع، ليس من الضروري نشر هذا الحقل. إذا قمنا بنشره، فإن عملية إزالة التسلسل ستنشئ كائن [Product] مع حقل [category] يشير إلى كائن [Category] تم إنشاؤه حديثًا. بالنسبة لـ n منتجًا، سيكون لدينا n كائنات [Category] تم إنشاؤها، في حين أن هناك حاجة إلى كائن واحد فقط. علاوة على ذلك، لن يشير حقل [category] للمنتجات إلى كائن [Category] الصحيح، وهو الكائن الذي تنتمي إليه. لذلك، تحتوي المنتجات هنا على حقل [category==null]؛
- في فئتي [Category] و [Product]، يتم تعريف حقل [EntityType entityType] على النحو التالي:
protected EntityType entityType = EntityType.POJO;
وبالتالي، فإن كيانات [Category] و [Product] التي تم إنشاؤها عن طريق التسلسل هي جميعها من نوع POJO.
- السطر 11: نقوم بحفظ الفئات. لا ينبغي أن يعمل هذا. في الواقع، بينما لا يُعد حقل [Product.category] ضروريًا للحفظ في تطبيق JDBC (يُستخدم حقل [categoryId] بدلاً منه)، فإنه ضروري تمامًا في تطبيق JPA. يجب أن يشير هذا الحقل إلى كيان [Category]، ولكنه هنا فارغ.
دعونا نفحص كود طريقة [DaoCategorie.saveEntities] في طبقة [DAO / JPA]:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// on note les produits qui vont être insérés
List<Produit> insertedProduits = new ArrayList<Produit>();
for (Categorie categorie : categories) {
EntityType categorieType = categorie.getEntityType();
List<Produit> produits = null;
if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
for (Produit produit : produits) {
if (produit.getId() == null) {
insertedProduits.add(produit);
}
// on en profite pour rétablir (si besoin est) la relation produit --> categorie
produit.setCategorie(categorie);
}
}
}
// on persiste les catégories / produits
try {
categoriesRepository.save(categories);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// on met à jour le champ [idCategorie] des produits insérés
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// résultat
return categories;
}
- السطران 13–14: يمكننا أن نرى أن العلاقة [Product] → [Category] قد أُعيد تأسيسها لكيانات POJO (السطر 8)، وهو ما يحدث هنا. وهذا يفسر سبب نجاح استمرارية الفئات. هذه الطريقة مفيدة في حالات أخرى: لا يمكنك أبدًا التأكد من أن المستخدم قد ربط المنتجات بالفئات بشكل صحيح. لذا نقوم بذلك نيابة عنهم؛
الآن دعونا نفحص طريقة [ProductController.saveProducts] التي تحافظ على المنتجات:
@RequestMapping(value = "/saveProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Response<List<CoreProduit>> saveProduits(HttpServletRequest request) {
...
// retrieve the posted value
String body = CharStreams.toString(request.getReader());
// we deserialize it
ObjectMapper mapper = context.getBean("jsonMapperShortProduit", ObjectMapper.class);
List<Produit> produits = mapper.readValue(body, new TypeReference<List<Produit>>() {
});
// we persist products
produits = daoProduit.saveEntities(produits);
List<CoreProduit> coreProduits = new ArrayList<CoreProduit>();
for (Produit produit : produits) {
coreProduits.add(new CoreProduit(produit.getId()));
}
// we return the answer
return new Response<List<CoreProduit>>(0, null, coreProduits);
...
}
- السطر 8: يتم إعادة إنشاء كائن List<Product> من القيمة المرسلة. وللأسباب الموضحة سابقًا، سيحتوي كل كائن [Product] على حقل:
- [EntityType entityType] يساوي [EntityType.POJO]؛
- [Category category] يساوي null؛
- السطر 11: من المفترض أن تفشل عملية الاحتفاظ بالمنتجات. في الواقع، مع JPA، لا يمكن الاحتفاظ بمنتج إلا إذا كان حقل [category] الخاص به يشير إلى كيان [Category]؛
دعونا نلقي نظرة على كود طريقة [DaoProduit.saveEntities] في طبقة [DAO / JPA]:
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
// on rétablit (si besoin est) le lien entre un produits et sa catégorie
for (Produit produit : entities) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// on persiste les produits
try {
return Lists.newArrayList(produitsRepository.save(entities));
} catch (Exception e) {
throw new DaoException(111, e, simpleClassName);
}
}
- الأسطر 3–8: لكل [Product] من نوع POJO، يتم إنشاء رابط إلى كائن [Category] بالمفتاح الأساسي الصحيح ونسخة غير فارغة. وهذا يكفي لطبقة JPA لتخزين المنتج بشكل صحيح؛
دعونا نلقي نظرة على نقطة أخيرة. تحتوي كائنات [Category] و[Product] على حقل إضافي [EntityType entityType] سيتم تسلسله إلى JSON عند إرسال هذه الكائنات إلى العميل. يمكننا التحقق من ذلك باستخدام [Advanced Rest Client]:
![]() |
على جانب العميل، تم تعريف كيانات [Category] و [Product] بدون الحقل [EntityType entityType]. وهذا أمر طبيعي لأن كائني [Category] و [Product] يتم تسلسلهما بدون أجزاء PROXY الخاصة بهما [Category.products] و [Product.category]. لذلك، لا يوجد على جانب العميل مفهوم لكيان PROXY. لا يوجد سوى كائنات عادية.
على جانب العميل، يتم استلام سلسلة JSON [1] بواسطة الطريقة [DaoCategorie.getAllShortEntities] التالية:
@Override
public List<Categorie> getAllShortEntities() {
...
// filters jSON
ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
// get all categories
Object map = client.<List<Categorie>, Void> getResponse("/getAllShortCategories", HttpMethod.GET, 202, null);
// the List<Categorie> category list
return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
});
...
}
- السطر 5: نقوم بتكوين أداة تعيين JSON الخاصة بكائن [RestTemplate] لمعالجة مرشحات JSON [jsonFilterCategorie] الخاصة بكائن [Category] والمرشح [jsonFilterProduct] الخاص بكائن [Product]؛
- السطر 7: يتم تسلسل/إلغاء تسلسل القيمة المرسلة (لا توجد قيمة هنا) والقيمة المستلمة (List<Category>) باستخدام أداة التعيين هذه. لاحظ أن وجود حقل [entityType] في سلسلة JSON المستلمة — على الرغم من أن هذا الحقل غير موجود في كيانات [Category] و [Product] على جانب العميل — لا يتسبب في حدوث خطأ. يتم تجاهله. لو كان قد تسبب في حدوث خطأ، لكنا قد عدّلنا مرشحات جانب العميل لتجاهله.
18.8. تنفيذ خدمة الويب / JSON / JPA / EclipseLink
لتنفيذ خدمة الويب / JSON / JPA / EclipseLink، ما عليك سوى تغيير تنفيذ JPA:
![]() |
ملاحظة: اضغط على Alt-F5، ثم أعد إنشاء جميع مشاريع Maven.
سنقوم بتشغيل خدمة الويب باستخدام تكوين وقت التشغيل [spring-webjson-server-jpa-generic-hibernate-eclipselink] المستخدم بالفعل لـ Hibernate. بمجرد الانتهاء من ذلك، قم بتشغيل الاختبارات الثلاثة للعميل العام [spring-webjson-client-generic]:
![]() |
![]() |
- في [1]، اختبار [JUnitTestCheckArguments]؛
- في [2]، اختبار [JUnitTestDao]؛
- في [3]، اختبار [JUnitTestPushTheLimits] الذي يتم تشغيله على جانب العميل (مشروع [spring-webjson-client-generic])؛
- في [4]، اختبار [JUnitTestPushTheLimits] الذي يتم تشغيله على جانب الخادم (تكوين التشغيل [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink])؛
18.9. تنفيذ خدمة الويب / JSON / JPA / OpenJpa
لتنفيذ خدمة الويب / JSON / JPA / OpenJPA، ما عليك سوى تغيير تنفيذ JPA:
![]() |
ملاحظة: اضغط على Alt-F5، ثم أعد إنشاء جميع مشاريع Maven.
سنقوم بتشغيل خدمة الويب باستخدام تكوين وقت التشغيل [spring-webjson-server-jpa-generic-openpa]:
![]() | ![]() |
بمجرد الانتهاء من ذلك، قم بتشغيل الاختبارات الثلاثة للعميل العام [spring-webjson-client-generic]:
![]() |
![]() |
- في [1]، اختبار [JUnitTestCheckArguments] (تكوين التشغيل [spring-webjson-client-generic-JUnitTestCheckArguments])؛
- في [2]، اختبار [JUnitTestDao] (تكوين التشغيل [spring-webjson-client-generic-JUnitTestDao])؛
- في [3]، اختبار [JUnitTestPushTheLimits] الذي يتم تشغيله على جانب العميل (تكوين التشغيل [spring-webjson-client-generic-JUnitTestPushTheLimits])؛
- في [4]، اختبار [JUnitTestPushTheLimits] الذي تم تنفيذه على جانب الخادم (تكوين التنفيذ [spring-jpa-generic-JUnitTestPushTheLimits-openpa])؛
لجعل الاختبارات تعمل، كان لا بد من إجراء تغييرات على طبقة DAO/JPA. في الواقع، ولسبب غير مفهوم، فشلت الطريقتان [DaoCategorie.saveEntities] و [DaoProduit.saveEntities] عند ملء قاعدة البيانات، مما يشير إلى أنه لم يكن من الممكن الاحتفاظ بالكيانات المنفصلة. الكيان المنفصل هو كيان يحتوي على:
- مفتاح أساسي غير فارغ؛
- إصدار غير فارغ؛
لم يتم التحقق من أي من هاتين الحالتين. ولعدم معرفتي أين أبحث، قمت بنسخ الكيانات المراد الاحتفاظ بها إلى قائمة جديدة تمامًا، وعندها نجحت الاختبارات. كان من الممكن إجراء هذا التغيير إما:
- في طبقة [DAO / JPA]؛
- في طبقة [الويب] التي تنشئ الكيانات المراد الاحتفاظ بها؛
اخترت القيام بذلك في طبقة [DAO / JPA]. هناك، بالطبع، انخفاض في الأداء، ولكنه ضئيل للغاية مقارنة بأوقات استجابة نظام إدارة قواعد البيانات (DBMS). التغييرات هي كما يلي:
في فئة [DaoCategorie] لمشروع [spring-jpa-generic]:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// ***************************************************************************************
// on clone la liste des catégories -- nécessaire parfois pour OpenJpa -- bug non compris
// ***************************************************************************************
List<Categorie> categories2 = new ArrayList<Categorie>();
for (Categorie categorie : categories) {
// catégorie
Categorie categorie2 = new Categorie(categorie.getId(), categorie.getVersion(), categorie.getNom(), null);
EntityType categorieType = categorie.getEntityType();
categorie2.setEntityType(categorieType);
categories2.add(categorie2);
// produits
List<Produit> produits = null;
if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
List<Produit> produits2 = new ArrayList<Produit>();
for (Produit produit : produits) {
Produit produit2 = new Produit(produit.getId(), produit.getVersion(), produit.getNom(),
produit.getIdCategorie(), produit.getPrix(), produit.getDescription(), produit.getCategorie());
produit2.setEntityType(produit.getEntityType());
produits2.add(produit2);
}
categorie2.setProduits(produits2);
}
}
// on note les produits qui vont être insérés
List<Produit> insertedProduits = new ArrayList<Produit>();
for (Categorie categorie : categories2) {
EntityType categorieType = categorie.getEntityType();
List<Produit> produits = null;
if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
for (Produit produit : produits) {
if (produit.getId() == null) {
insertedProduits.add(produit);
}
// on en profite pour rétablir (si besoin est) la relation produit --> categorie
produit.setCategorie(categorie);
}
}
}
// on persiste les catégories / produits
try {
categoriesRepository.save(categories2);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// on met à jour le champ [idCategorie] des produits insérés
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// résultat
return categories2;
}
- الأسطر 3–25: يتم نسخ قائمة [categories] التي تم استلامها كمعلمة (السطر 2) في قائمة [categories2] (السطر 6). وهذه هي القائمة التي يتم الاحتفاظ بها وإرجاعها إلى المستدعي (السطر 52). ولهذا نتيجة مهمة: يتم إرجاع قائمة مختلفة عن تلك التي تم تمريرها كمعلمة، لذا حيث كان بإمكاننا سابقًا كتابة:
List<Categorie> categories=...
daoCategorie.saveEntities(categories)
// exploitation de [categories]
يجب أن نكتب الآن:
List<Categorie> categories=...
categories=daoCategorie.saveEntities(categories)
// exploitation de [categories]
في فئة [DaoProduct] لمشروع [spring-jpa-generic]، تم تعديل طريقة [saveEntities] بشكل مشابه:
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
// ***************************************************************************************
// on clone la liste des produits -- nécessaire parfois pour OpenJpa -- bug non compris
// ***************************************************************************************
List<Produit> produits2 = new ArrayList<Produit>();
for (Produit produit : entities) {
Produit produit2 = new Produit(produit.getId(), produit.getVersion(), produit.getNom(), produit.getIdCategorie(),
produit.getPrix(), produit.getDescription(), produit.getCategorie());
produit2.setEntityType(produit.getEntityType());
produits2.add(produit2);
}
// on rétablit (si besoin est) le lien entre un produits et sa catégorie
for (Produit produit : produits2) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// on persiste les produits
try {
return Lists.newArrayList(produitsRepository.save(produits2));
} catch (Exception e) {
throw new DaoException(111, e, simpleClassName);
}
}
18.10. تنفيذ خدمة الويب / JSON / JPA / EclipseLink / PostgreSQL
لتنفيذ خدمة الويب / JSON / JPA / EclipseLink / PostgreSQL، يجب تثبيت:
- مشروع [postgresql-config-jdbc] لتكوين طبقة PostgreSQL JDBC؛
- مشروع [postgresql-config-jpa-eclipselink] لتكوين طبقة JPA لـ PostgreSQL؛
- اضغط على Alt-F5 وأعد إنشاء جميع مشاريع Maven؛
![]() |
قم بتشغيل نظام إدارة قواعد البيانات PostgreSQL وابدأ تشغيل خدمة الويب باستخدام تكوين وقت التشغيل [spring-webjson-server-jpa-generic-hibernate-eclipselink] الذي تم استخدامه سابقًا. وبمجرد الانتهاء من ذلك، قم بتشغيل الاختبارات الثلاثة الخاصة بالعميل العام [spring-webjson-client-generic]:
![]() |
![]() |
- في [1]، اختبار [JUnitTestCheckArguments] (تكوين التشغيل [spring-webjson-client-generic-JUnitTestCheckArguments])؛
- في [2]، اختبار [JUnitTestDao] (تكوين التشغيل [spring-webjson-client-generic-JUnitTestDao])؛
- في [3]، اختبار [JUnitTestPushTheLimits] الذي تم تنفيذه على جانب العميل (تكوين التشغيل [spring-webjson-client-generic-JUnitTestPushTheLimits])؛
- في [4]، اختبار [JUnitTestPushTheLimits] الذي تم تنفيذه على جانب الخادم (تكوين التنفيذ [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink])؛







































