13. [Cours]: عرض قاعدة بيانات على الويب باستخدام Spring MVC
الكلمات الرئيسية: بنية متعددة الطبقات، Spring، حقن التبعيات، خدمة الويب / jSON، العميل / الخادم
13.1. Support
![]() | ![]() |
ستجد مشاريع هذا الفصل في المجلد [support / chap-13]. يتيح البرنامج النصي SQL [dbintrospringdata.sql] إنشاء قاعدة البيانات MySQL اللازمة للاختبارات.
13.2. مكانة Spring MVC في تطبيق الويب
لنحدد مكان Spring MVC في تطوير تطبيق ويب. في أغلب الأحيان، سيتم بناء هذا التطبيق على بنية متعددة الطبقات مثل التالية:
![]() |
- الطبقة [Web] هي الطبقة التي تتعامل مع مستخدم تطبيق الويب. يتفاعل المستخدم مع تطبيق الويب من خلال صفحات الويب التي يتم عرضها بواسطة متصفح. يقع Spring MVC في هذه الطبقة وفقط في هذه الطبقة؛
- تنفذ الطبقة [métier] قواعد إدارة التطبيق، مثل حساب الراتب أو الفاتورة. تستخدم هذه الطبقة البيانات الواردة من المستخدم عبر الطبقة [Web] ومن SGBD عبر الطبقة [DAO]؛
- تدير الطبقة [DAO] (كائنات الوصول إلى البيانات)، والطبقة [ORM] (مخطط العلاقات بين الكائنات)، وبرنامج التشغيل JDBC الوصول إلى بيانات SGBD. تشكل الطبقة [ORM] جسرًا بين الكائنات التي تعالجها الطبقة [DAO] والصفوف والأعمدة في جداول قاعدة البيانات العلائقية. تسمح المواصفة JPA (Java Persistence API) بالتجريد من ORM المستخدم إذا كان هذا الأخير يطبق هذه المواصفات. سيكون هذا هو الحال هنا وسنطلق من الآن فصاعدًا على الطبقة ORM اسم الطبقة JPA؛
- يتم دمج الطبقات بواسطة إطار عمل Spring؛
13.3. نموذج تطوير Spring MVC
يقوم Spring MVC بتنفيذ نموذج الهندسة المعمارية المسمى MVC (النموذج – العرض – وحدة التحكم) بالطريقة التالية:
![]() |
تتم معالجة طلب العميل على النحو التالي:
- الطلب - تكون URL المطلوبة بالشكل http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... يستخدم [Front Controller] ملف تكوين أو تعليقات Java لتوجيه الطلب إلى وحدة التحكم الصحيحة والإجراء الصحيح داخل وحدة التحكم هذه. وللقيام بذلك، يستخدم الحقل [Action] في URL. أما باقي أجزاء URL و [/param1/param2/...] فهي تتكون من معلمات اختيارية سيتم تمريرها إلى الإجراء. يكون C في MVC هنا هو السلسلة [Front Controller, Contrôleur, Action]. إذا لم يتمكن أي وحدة تحكم من معالجة الإجراء المطلوب، فسيرد خادم الويب بأن URL المطلوب لم يتم العثور عليه.
- معالجة
- يمكن للإجراء المختار استخدام المعلمات parami التي أرسلها إليه [Front Controller]. وقد تأتي هذه المعلمات من عدة مصادر:
- مسار [/param1/param2/...] الخاص بـ URL،
- من المعلمات [p1=v1&p2=v2] الخاصة بـ URL,
- من المعلمات التي أرسلها المتصفح مع طلبه؛
- في معالجة طلب المستخدم، قد تحتاج العملية إلى الطبقة [métier] [2b]. بمجرد معالجة طلب العميل، يمكن أن يستدعي هذا الطلب استجابات متنوعة. ومن الأمثلة النموذجية على ذلك:
- صفحة خطأ إذا تعذر معالجة الطلب بشكل صحيح
- صفحة تأكيد في الحالات الأخرى
- تطلب الإجراء عرض طريقة عرض معينة [3]. ستعرض طريقة العرض هذه البيانات التي نسميها نموذج طريقة العرض. هذا هو M في MVC. ستقوم الإجراء بإنشاء هذا النموذج M [2c] وتطلب عرض طريقة عرض V [3]؛
- الاستجابة - تستخدم طريقة العرض V المختارة النموذج M الذي أنشأته الإجراء لتهيئة الأجزاء الديناميكية من الاستجابة HTML التي يجب أن ترسلها إلى العميل ثم ترسل هذه الاستجابة.
بالنسبة لخدمة الويب / jSON، يتم تعديل البنية السابقة بشكل طفيف:
![]() |
- في [4a]، يتم تحويل النموذج الذي هو فئة Java إلى سلسلة jSON بواسطة مكتبة jSON؛
- في [4b]، يتم إرسال هذه السلسلة jSON إلى المتصفح؛
الآن، دعونا نوضح العلاقة بين بنية الويب MVC وبنية الطبقات. وفقًا للتعريف الذي نضعه للنموذج، قد يكون هذان المفهومان مرتبطين أو غير مرتبطين. لنأخذ تطبيق ويب Spring MVC أحادي الطبقة كمثال:
![]() |
إذا قمنا بتنفيذ الطبقة [Web] باستخدام Spring MVC، فسنحصل على بنية ويب MVC ولكن ليس بنية متعددة الطبقات. هنا، ستتولى الطبقة [web] كل شيء: العرض، والوظيفة، والوصول إلى البيانات. هذه هي الإجراءات التي ستقوم بهذا العمل.
الآن، لننظر إلى بنية ويب متعددة الطبقات:
![]() |
يمكن تنفيذ الطبقة [Web] بدون إطار عمل وبدون اتباع النموذج MVC. لدينا إذن بنية متعددة الطبقات، لكن الطبقة الويب لا تنفذ النموذج MVC.
على سبيل المثال، في عالم .NET، يمكن تنفيذ الطبقة [Web] المذكورة أعلاهأعلاه يمكن تنفيذها باستخدام ASP.NET و MVC، وبذلك نحصل على بنية طبقات تحتوي على طبقة [Web] من النوع MVC. وبعد ذلك، يمكن استبدال هذه الطبقة ASP.NET MVC بطبقة ASP.NET تقليدية (WebForms) مع الحفاظ على بقية (المهنة، DAO، ORM) كما هو. نحصل عندئذٍ على بنية طبقات تحتوي على طبقة [Web] التي لم تعد من النوع MVC.
في MVC، ذكرنا أن النموذج M هو نموذج العرض V، c.a.d. مجموعة البيانات المعروضة بواسطة العرض V. يتم تقديم تعريف آخر للنموذج M لـ MVC:
![]() |
يعتبر العديد من المؤلفين أن ما يقع على يمين الطبقة [Web] يشكل النموذج M لـ MVC. لتجنب الغموض، يمكننا التحدث عن:
- نموذج المجال عند الإشارة إلى كل ما يقع على يمين الطبقة [Web]
- نموذج العرض عند الإشارة إلى البيانات المعروضة بواسطة عرض V
فيما يلي، سيشير مصطلح "نموذج M" حصريًا إلى نموذج عرض V.
13.4. مشروع ويب / jSON مع Spring MVC
يقدم موقع [http://spring.io/guides] دروسًا تعليمية للمبتدئين لاكتشاف نظام Spring. سنتبع إحدى هذه الدروس لاكتشاف تكوين Maven اللازم لمشروع Spring MVC.
13.4.1. مشروع العرض التوضيحي
![]() |
- في [1]، نقوم باستيراد أحد أدلة Spring؛
![]() |
- في [2]، نختار المثال [Rest Service]؛
- في [3]، نختار مشروع Maven؛
- في [4]، نأخذ النسخة النهائية من الدليل؛
- في [5]، نقوم بالتحقق؛
- في [6]، المشروع المستورد؛
غالبًا ما يُطلق على الخدمات الويب التي يمكن الوصول إليها عبر معايير URL والتي تقدم نصًا jSON اسم خدمات REST (REpresentational State Transfer). يُطلق على الخدمة اسم Restful إذا كانت تتوافق مع قواعد معينة.
لنلقِ نظرة الآن على المشروع المستورد، بدءًا من تكوين Maven الخاص به.
13.4.2. تكوين Maven
ملف [pom.xml] هو التالي:
<?xml version="1.0" encoding="UTF-8"?>
<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>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</pluginRepository>
</pluginRepositories>
</project>
- الأسطر 6-8: خصائص مشروع Maven. ينقص علامة [<packaging>] التي تشير إلى نوع الملف الناتج عن تجميع Maven. في حالة عدم وجودها، يتم استخدام النوع [jar]. وبالتالي، فإن التطبيق هو تطبيق قابل للتنفيذ من نوع وحدة التحكم، وليس تطبيق ويب حيث يكون التعبئة [war]؛
- الأسطر 10-14: يحتوي مشروع Maven على مشروع أبوي [spring-boot-starter-parent]. وهو الذي يحدد معظم تبعيات المشروع. قد تكون هذه التبعيات كافية، وفي هذه الحالة لا نضيف المزيد، أو قد لا تكون كافية، وفي هذه الحالة نضيف التبعيات الناقصة؛
- الأسطر 17-20: يجلب الأرتيفاكت [spring-boot-starter-web] معه المكتبات اللازمة لمشروع Spring MVC من نوع خدمة الويب حيث لا توجد عروض مُنشأة. يحتوي هذا الأرتيفاكت على عدد كبير جدًا من المكتبات، بما في ذلك مكتبات خادم Tomcat المدمج. سيتم تشغيل التطبيق على هذا الخادم؛
المكتبات التي توفرها هذه التهيئة كثيرة جدًا:
![]() | ![]() |
فيما يلي نرى الأرشيفات الثلاثة لخادم Tomcat.
13.4.3. بنية خدمة Spring [web / jSON]
بالنسبة لخدمة الويب / jSON، يقوم Spring MVC بتنفيذ نموذج MVC بالطريقة التالية:
![]() |
- في [4a]، يتم تحويل النموذج الذي هو فئة Java إلى سلسلة jSON بواسطة مكتبة jSON؛
- إلى [4b]، يتم إرسال هذه السلسلة jSON إلى المتصفح؛
13.4.4. وحدة التحكم C
![]() |
يحتوي التطبيق المستورد على وحدة التحكم التالية:
package hello;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
@RequestMapping("/greeting")
public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}
}
- السطر 9: التعليق التوضيحي [@RestController] يجعل من الفئة [GreetingController] وحدة تحكم Spring، أي أن أساليبها مسجلة لمعالجة URL. لقد رأينا التعليق التوضيحي المماثل [@Controller]. كانت نتيجة أساليب هذا المتحكم هي نوع [String] الذي كان اسم العرض المراد عرضه. الأمر مختلف هنا. تُرجع أساليب وحدة التحكم من النوع [@RestController] كائنات يتم تسلسلها لإرسالها إلى المتصفح. يعتمد نوع التسلسل الذي يتم إجراؤه على تكوين Spring MVC. هنا، سيتم تسلسلها إلى jSON. إن وجود مكتبة jSON في تبعيات المشروع هو ما يجعل Spring Boot يقوم، عن طريق التكوين التلقائي، بتكوين المشروع بهذه الطريقة؛
- السطر 14: تشير التعليقة التوضيحية [@RequestMapping] إلى URL الذي تعالجه الطريقة، وهنا URL [/greeting]؛
- السطر 15: سبق أن شرحنا التعليق التوضيحي [@RequestParam]. والنتيجة التي تُرجعها الطريقة هي كائن من النوع [Greeting].
- السطر 12: عدد صحيح طويل من النوع الذري. وهذا يعني أنه يدعم التنافس في الوصول. قد ترغب عدة خيوط في زيادة المتغير [counter] في نفس الوقت. وسيتم ذلك بشكل صحيح. لا يمكن لخيط قراءة قيمة العداد إلا إذا كان الخيط الذي يقوم بتعديله قد انتهى من تعديله.
13.4.5. النموذج M
النموذج M الناتج عن الطريقة السابقة هو الكائن [Greeting] التالي:
![]() |
package hello;
public class Greeting {
private final long id;
private final String content;
public Greeting(long id, String content) {
this.id = id;
this.content = content;
}
public long getId() {
return id;
}
public String getContent() {
return content;
}
}
ستنشئ عملية التحويل jSON لهذا الكائن سلسلة الأحرف {"id":n,"content":"نص"}. في النهاية، ستكون سلسلة jSON الناتجة عن طريقة وحدة التحكم بالشكل التالي:
أو
13.4.6. التنفيذ
![]() |
الفئة [Application.java] هي الفئة القابلة للتنفيذ في المشروع. ورمزها هو كما يلي:
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
لقد سبق أن تناولنا هذا الرمز وشرحناه في المثال السابق.
13.4.7. تنفيذ المشروع
لنقم بتنفيذ المشروع:
![]() |
نحصل على سجلات وحدة التحكم التالية:
- السطر 13: يبدأ تشغيل خادم Tomcat على المنفذ 8080 (السطر 12)؛
- السطر 17: السيرفلت [DispatcherServlet] موجود؛
- السطر 20: تم اكتشاف الطريقة [GreetingController.greeting]؛
لاختبار تطبيق الويب، نطلب URL [http://localhost:8080/greeting]:
![]() | ![]() |
نتلقى بالفعل السلسلة المتوقعة jSON. قد يكون من المثير للاهتمام رؤية الرؤوس HTTP المرسلة من الخادم. للقيام بذلك، سنستخدم ملحق Chrome المسمى [Advanced Rest Client] (Chrome / Ctrl-T / قائمة [Applications] / [Advanced Rest Client] - انظر الملحقات الفقرة 22.5):
![]() |
- في [1]، URL المطلوب؛
- في [2]، تُستخدم الطريقة GET؛
- في [3]، الرد jSON؛
- في [4]، أشار الخادم إلى أنه يرسل استجابة بتنسيق jSON؛
- في [5]، يُطلب نفس URL ولكن هذه المرة مع POST؛
- في [7]، يتم إرسال المعلومات إلى الخادم في شكل [urlencoded]؛
- في [6]، المعلمة name مع قيمتها؛
- في [8]، يُعلم المتصفح الخادم بأنه يرسل إليه معلومات [urlencoded]؛
- في [9]، رد الخادم jSON؛
13.4.8. إنشاء أرشيف قابل للتنفيذ
نقوم الآن بإنشاء أرشيف قابل للتنفيذ:
![]() |
![]() |
- في [1]: يتم تنفيذ هدف Maven؛
- في [2]: هناك هدفان (goals): [clean] لحذف المجلد [target] من مشروع Maven، و[package] لإعادة إنشائه؛
- في [3]: سيتم إنشاء المجلد [target] في هذا المجلد؛
- في [4]: يتم إنشاء الهدف؛
في السجلات التي تظهر في وحدة التحكم، من المهم أن يظهر المكون الإضافي [spring-boot-maven-plugin]. فهو الذي يقوم بإنشاء الأرشيف القابل للتنفيذ.
باستخدام وحدة التحكم، ننتقل إلى المجلد الذي تم إنشاؤه:
D:\Temp\wksSTS\gs-rest-service\target>dir
...
11/06/2014 15:30 <DIR> classes
11/06/2014 15:30 <DIR> generated-sources
11/06/2014 15:30 11 073 572 gs-rest-service-0.1.0.jar
11/06/2014 15:30 3 690 gs-rest-service-0.1.0.jar.original
11/06/2014 15:30 <DIR> maven-archiver
11/06/2014 15:30 <DIR> maven-status
...
- السطر 5: الأرشيف الذي تم إنشاؤه؛
يتم تشغيل هذا الملف المضغوط بالطريقة التالية:
D:\Temp\wksSTS\gs-rest-service-complete\target>java -jar gs-rest-service-0.1.0.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.1.0.RELEASE)
2014-06-11 15:32:47.088 INFO 4972 --- [ main] hello.Application
: Starting Application on Gportpers3 with PID 4972 (D:\Temp\wk
sSTS\gs-rest-service-complete\target\gs-rest-service-0.1.0.jar started by ST in
D:\Temp\wksSTS\gs-rest-service-complete\target)
...
الآن بعد تشغيل تطبيق الويب، يمكننا الوصول إليه باستخدام متصفح:
![]() |
13.4.9. نشر التطبيق على خادم Tomcat
كما تم في المشروع السابق، نقوم بتعديل الملف [pom.xml] بالطريقة التالية:
<?xml version="1.0" encoding="UTF-8"?>
<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>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<packaging>war</packaging>
...
</project>
- السطر 9: يجب الإشارة إلى أننا سنقوم بإنشاء أرشيف war (Web ARchive)؛
كما يجب تكوين تطبيق الويب. في حالة عدم وجود ملف [web.xml]، يتم ذلك باستخدام فئة موروثة من [SpringBootServletInitializer]:
![]() |
الفئة [ApplicationInitializer] هي كما يلي:
package hello;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
public class ApplicationInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
- السطر 6: الفئة [ApplicationInitializer] توسع الفئة [SpringBootServletInitializer]؛
- السطر 9: يتم إعادة تعريف الطريقة [configure] (السطر 8)؛
- السطر 10: يتم تحديد الفئة التي تهيئ المشروع؛
لتنفيذ المشروع، يمكن اتباع الخطوات التالية:
![]() |
- في [1-2]، يتم تنفيذ المشروع على أحد الخوادم المسجلة في IDE Eclipse؛
بعد ذلك، يمكن طلب URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] في متصفح:
![]() |
13.4.10. الخلاصة
لقد قدمنا نوعًا من مشاريع Spring MVC حيث ترسل تطبيق الويب دفقًا jSON إلى المتصفح. سنقوم الآن بتطوير تطبيق ويب / jSON لعرض قاعدة البيانات [dbintrospringdata] التي تمت دراستها في البرنامج التعليمي [Introduction à Spring Data] على الويب.
13.5. عرض قاعدة البيانات [dbintrospringdata] على الويب
13.5.1. بنية خدمة الويب / jSON
سنقوم بتنفيذ البنية التالية:
![]() |
يتم تنفيذ الطبقتين [DAO] و [JPA] بواسطة التطبيق المكتوب في البرنامج التعليمي [Introduction à Spring Data].
13.5.2. تثبيت قاعدة البيانات
![]() |
يتيح البرنامج النصي SQL [dbintrospringdata.sql] إنشاء قاعدة البيانات MySQL اللازمة للاختبارات.
13.5.3. مشروع Eclipse لخدمة الويب / jSON
مشروع Eclipse لخدمة الويب / jSON هو التالي:
![]() |
إنه مشروع 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>istia.st.webjson</groupId>
<artifactId>intro-server-webjson01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>intro-server-webjson01</name>
<description>démo spring mvc</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>istia.st.springdata</groupId>
<artifactId>intro-spring-data-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- الأسطر 11-15: مشروع Maven الأصلي المستخدم بالفعل للطبقة [DAO]؛
- الأسطر 18-22: التبعية لطبقة [DAO]؛
- الأسطر 23-26: التبعية على الأرتيفاكت [spring-boot-starter-web]. يجلب هذا الأرتيفاكت معه جميع التبعيات اللازمة لإنشاء خدمة ويب / jSON. كما يجلب مكتبات غير ضرورية. لذلك، سيكون من الضروري إجراء تكوين أكثر دقة. لكن هذا التكوين مفيد للبدء؛
- السطور 28-30: التبعية على الأداة [spring-boot-starter] تسمح بإدارة تعليقات Spring Boot؛
التبعيات التي يجلبها هذا التكوين هي التالية:
![]() |
- في [1]، نرى أن Eclipse قد رصد التبعية على أرشيف مشروع [intro-spring-data-01]؛
التبعيات المذكورة أعلاه هي تبعيات كل من الطبقة [DAO] والطبقة [web].
13.5.3.1. تكوين الطبقة [web]
يتم تكوين الطبقة [web] بواسطة ملف [AppConfig]:
![]() |
تقوم الفئة [WebConfig] بتكوين الطبقة [web]:
package spring.webjson.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
// -------------------------------- تكوين الطبقة [web]
@Autowired
private ApplicationContext context;
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// مرشحات jSON
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategorieWithProduits")
public ObjectMapper jsonMapperCategorieWithProduits() {
// أداة التعيين jSON
ObjectMapper mapper = new ObjectMapper();
// مرشحات
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// النتيجة
return mapper;
}
@Bean(name = "jsonMapperProduitWithCategorie")
public ObjectMapper jsonMapperProduitWithCategorie() {
// مُخطِط jSON
ObjectMapper mapper = new ObjectMapper();
// مرشحات
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// النتيجة
return mapper;
}
@Bean(name = "jsonMapperCategorieWithoutProduits")
public ObjectMapper jsonMapperCategorieWithoutProduits() {
// مُخطِط jSON
ObjectMapper mapper = new ObjectMapper();
// مرشحات
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// النتيجة
return mapper;
}
@Bean(name = "jsonMapperProduitWithoutCategorie")
public ObjectMapper jsonMapperProduitWithoutCategorie() {
// مُخطِط jSON
ObjectMapper mapper = new ObjectMapper();
// مرشحات
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// النتيجة
return mapper;
}
}
- السطر 18: التوضيح [@EnableWebMvc] يؤدي إلى تكوينات تلقائية لإطار عمل Spring MVC؛
- السطر 19: الفئة [WebConfig] توسع فئة Spring [WebMvcConfigurerAdapter] لإعادة تعريف بعض الفاصوليا (السطور 26-40)؛
- السطران 22-23: حقن سياق Spring؛
- الأسطر 25-29: تعريف سيرفلت إطار عمل Spring MVC، وهو الذي يوجه الطلبات HTTP إلى وحدة التحكم والطريقة الصحيحة. [DispatcherServlet] هي فئة من Spring؛
- الأسطر 31-34: يُشار إلى أن هذه الخدمة تعالج جميع URL؛
- الأسطر 36-39: إن وجود هذا البين هو الذي سيقوم بتنشيط خادم Tomcat الموجود في أرشيفات المشروع. وسوف ينتظر الطلبات على المنفذ 8080؛
- الأسطر 42-91: حبات ستُستخدم لإدارة مرشحات jSON؛
- الأسطر 42-45: مخطط jSON بدون مرشحات؛
- الأسطر 47-57: أداة التعيين jSON التي تتيح الحصول على فئة مع منتجاتها. تجدر الإشارة إلى أنه عند طلب فئة مع منتجاتها، يجب تكوين كل من مرشح jSON الخاص بالفئة [Categorie] ومرشح الفئة [Produit]. وهذا هو الحال دائمًا. عند تسلسل/إلغاء تسلسل فئة في jSON، يجب تكوين مرشح jSON للفئة ومرشحات جميع التبعيات المراد تضمينها فيها؛
- الأسطر 59-69: المُعين jSON الذي يسمح بالحصول على منتج مع فئته؛
- الأسطر 71-80: المُعَيِّن jSON الذي يسمح بالحصول على فئة بدون منتجاتها؛
- الأسطر 82-91: المُخطِط jSON الذي يسمح بالحصول على منتج بدون فئته؛
تقوم الفئة [AppConfig] بتكوين التطبيق بأكمله، أي الطبقات [web] و [DAO]:
package spring.webjson.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import spring.data.config.DaoConfig;
@ComponentScan(basePackages = { "spring.webjson" })
@Import({ DaoConfig.class, WebConfig.class})
public class AppConfig {
}
- السطر 9: يتم استيراد حبوب الطبقة [DAO] وحبوب الطبقة [web]؛
- السطر 8: يشير إلى الحزم التي يمكن العثور فيها على حبات Spring أخرى؛
تجدر الإشارة إلى أننا لم نستخدم التعليق التوضيحي [@EnableAutoConfiguration] في أي مكان. لقد فضلنا التحكم في التكوين بأنفسنا.
13.5.4. نموذج التطبيق
![]() |
الفئة [ApplicationModel] هي كما يلي:
package spring.webjson.models;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
@Component
public class ApplicationModel implements IDao {
// الطبقة [DAO]
@Autowired
private IDao dao;
@Override
public void addProduits(List<Produit> produits) {
dao.addProduits(produits);
}
@Override
public void deleteAllProduits() {
dao.deleteAllProduits();
}
@Override
public void updateProduits(List<Produit> produits) {
dao.updateProduits(produits);
}
@Override
public List<Produit> getAllProduits() {
return dao.getAllProduits();
}
@Override
public void addCategories(List<Categorie> categories) {
dao.addCategories(categories);
}
@Override
public void deleteAllCategories() {
dao.deleteAllCategories();
}
@Override
public void updateCategories(List<Categorie> categories) {
dao.updateCategories(categories);
}
@Override
public List<Categorie> getAllCategories() {
return dao.getAllCategories();
}
@Override
public Produit getProduitByIdWithCategorie(Long idProduit) {
return dao.getProduitByIdWithCategorie(idProduit);
}
@Override
public Produit getProduitByNameWithCategorie(String nom) {
return dao.getProduitByNameWithCategorie(nom);
}
@Override
public Categorie getCategorieByIdWithProduits(Long idCategorie) {
return dao.getCategorieByIdWithProduits(idCategorie);
}
@Override
public Categorie getCategorieByNameWithProduits(String nom) {
return dao.getCategorieByNameWithProduits(nom);
}
@Override
public Produit getProduitByIdWithoutCategorie(Long idProduit) {
return dao.getProduitByIdWithoutCategorie(idProduit);
}
@Override
public Categorie getCategorieByIdWithoutProduits(Long idCategorie) {
return dao.getCategorieByIdWithoutProduits(idCategorie);
}
@Override
public Produit getProduitByNameWithoutCategorie(String nom) {
return dao.getProduitByNameWithoutCategorie(nom);
}
@Override
public Categorie getCategorieByNameWithoutProduits(String nom) {
return dao.getCategorieByNameWithoutProduits(nom);
}
}
- السطر 12: الفئة هي فئة Spring فردية؛
- السطر 13: التي تنفذ واجهة [IDao] من الطبقة [DAO]؛
- السطران 16-17: حقن مرجع في الطبقة [DAO]؛
- السطور 19-99: تنفيذ واجهة [IDao]؛
تتطور بنية طبقة الويب على النحو التالي:
![]() |
- في [2b]، تتواصل أساليب وحدة التحكم أو وحدات التحكم مع العنصر الفردي [ApplicationModel]؛
توفر هذه الاستراتيجية مرونة في إدارة ذاكرة التخزين المؤقت المحتملة. يمكن استخدام الفئة [ApplicationModel] لتخزين المعلومات التي يتم الحصول عليها من الطبقة [DAO] أو بيانات التكوين. قد يكون ذلك مفيدًا عندما لا يكون لدينا سيطرة على الطبقة [DAO]. قد تتطور استراتيجية التخزين المؤقت هذه بمرور الوقت. لن تؤثر التعديلات على كود وحدة التحكم أو وحدات التحكم.
13.5.5. وحدة التحكم
![]() |
![]() |
لدينا هنا وحدة تحكم واحدة فقط، وهي الفئة [MyController].
13.5.5.1. URL المعروضة
فئات URL المعروضة بواسطة وحدة التحكم هذه هي التالية:
| يضيف منتجات إلى قاعدة البيانات. يتم نشرها. الرد هو السلسلة jSON وهي قائمة بالمنتجات المضافة مع مفتاحها الأساسي. |
| حذف جميع المنتجات من قاعدة البيانات. |
| يتم تحديث المنتجات في قاعدة البيانات. يتم نشرها. الرد هو سلسلة jSON من قائمة المنتجات المحدثة. |
| يحصل على السلسلة jSON من جميع المنتجات. |
| يضيف فئات إلى قاعدة البيانات. يتم نشر هذه الفئات. الرد هو السلسلة jSON من قائمة الفئات المضافة مع مفتاحها الأساسي. إذا كانت الفئات تحتوي على منتجات، يتم إضافتها أيضًا إلى قاعدة البيانات. |
| يحذف جميع الفئات من قاعدة البيانات بالإضافة إلى جميع المنتجات الموجودة فيها. بعد ذلك تصبح قاعدة البيانات فارغة. |
| يُحدّث الفئات في قاعدة البيانات. يتم نشر هذه الفئات. والرد هو قائمة بالفئات التي تم تحديثها. إذا كانت الفئات تحتوي على منتجات، يتم تحديث هذه المنتجات أيضًا في قاعدة البيانات. يُرجع السلسلة jSON للفئات التي تم تعديلها؛ |
| يحصل على السلسلة jSON من جميع الفئات. |
| يحصل على السلسلة jSON لمنتج محدد بواسطة معرّفه، مع فئته. |
| يحصل على السلسلة jSON لمنتج محدد بواسطة معرّفه، بدون فئته. |
| يحصل على السلسلة jSON لمنتج محدد باسمه، مع فئته. |
| يحصل على السلسلة jSON لمنتج محدد باسمه، بدون فئته. |
| يحصل على السلسلة jSON من فئة محددة بواسطة معرفها، مع منتجاتها. |
| يحصل على السلسلة jSON من فئة محددة باسمها، مع منتجاتها. |
| يحصل على السلسلة jSON من فئة محددة باسمها، بدون منتجاتها. |
| يحصل على السلسلة jSON من فئة محددة بواسطة معرفها بدون منتجاتها. |
تتوافق URL المعروضة مع طرق واجهة [IDao] للطبقة [DAO]. طرق خدمة الويب / jSON مبنية جميعها على نفس النموذج. سنقوم بفحص بعضها.
13.5.5.2. هيكل وحدة التحكم
هيكل وحدة التحكم هو كما يلي:
package spring.webjson.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.CharStreams;
import spring.data.dao.DaoException;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
import spring.webjson.models.ApplicationModel;
import spring.webjson.models.Response;
@Controller
public class MyController {
// تبعيات Spring
@Autowired
private ApplicationModel application;
// مرشحات jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
// الطبقة [MyController] هي فئة فردية ولا يتم إنشاء مثيل لها إلا مرة واحدة
public MyController() {
// System.out.println("MyController");
}
@RequestMapping(value = "/addProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addProduits(HttpServletRequest request) throws JsonProcessingException {
...
}
- السطر 28: التعليق التوضيحي [@Controller] يجعل الفئة مكونًا في Spring؛
- السطران 32-33: حقن مرجع في الفئة [ApplicationModel]؛
- الأسطر 36-50: حقن مراجع في المخططين jSON؛
- السطر 58: URL المعروضة هي [/addProduits]. يجب على العميل استخدام طريقة [POST] لتقديم طلبه (method = RequestMethod.POST). يجب عليه إرسال القيمة المرسلة في شكل سلسلة jSON (consumes = "application/json; charset=UTF-8"). تقوم الطريقة نفسها بإرجاع الرد إلى العميل (السطر 59). سيكون ذلك سلسلة أحرف (السطر 60). سيتم إرسال الرأس HTTP [Content-type : application/json; charset=UTF-8] إلى العميل لإعلامه بأنه سيتلقى سلسلة jSON (السطر 58)؛
- السطر 60: تعرض الطريقة [addProduits] السلسلة jSON من قائمة المنتجات المضافة إلى قاعدة البيانات؛
13.5.5.3. استجابة طرق وحدة التحكم
تُرجع جميع طرق وحدة التحكم استجابة من النوع [Response] كما يلي:
![]() |
package spring.webjson.service;
import java.util.List;
public class Response<T> {
// ----------------- الخصائص
// حالة العملية
private int status;
// رسائل الخطأ المحتملة
private List<String> messages;
// نص الرد
private T body;
// المنشئات
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// مُستردات ومُعيّنات
...
}
- السطر 5: الاستجابة تغلف نوع T؛
- السطر 13: الاستجابة من النوع T؛
- الأسطر 9-11: من الممكن أن تواجه إحدى الطرق استثناءً. في هذه الحالة، ستُرجع استجابةً تحتوي على:
- السطر 9: status!=0؛
- السطر 11: قائمة الأخطاء التي تمت مواجهتها؛
13.5.5.4. L'URL [/addProduits]
L'URL [/addProduits] تتم معالجتها بواسطة الطريقة التالية:
@RequestMapping(value = "/addProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addProduits(HttpServletRequest request) throws JsonProcessingException {
// الرد
Response<List<Produit>> response;
try {
// استرداد القيمة المرسلة
String body = CharStreams.toString(request.getReader());
List<Produit> produits = jsonMapperProduitWithoutCategorie.readValue(body, new TypeReference<List<Produit>>() {
});
// إعادة إنشاء الارتباط بين المنتجات والفئات
for (Produit produit : produits) {
produit.setCategorie(application.getCategorieByIdWithoutProduits(produit.getIdCategorie()));
}
// يتم حفظ المنتجات
application.addProduits(produits);
response = new Respon se<List<Produit>>(0, null, produits);
} catch (DaoException e1) {
response = new Response<List<Produit>>(1000, e1.getErreurs(), null);
} catch (Exception e2) {
response = new Response<List<Produit>>(1000, getErreursForException(e2), null);
}
// الرد jSON
return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
- السطر 3: تقبل الطريقة كمعلمة [HttpServletRequest request] التي تحتوي على جميع المعلومات المتعلقة بطلب العميل؛
- السطر 5: الرد الذي سيتم إرساله إلى العميل: قائمة بالمنتجات؛
- السطر 8: يتم استرداد القيمة المرسلة. تنتمي الفئة [CharStreams] إلى المكتبة [Google Guava] التي تمت إضافة مرجعها في الملف [pom.xml]. نحصل على السلسلة jSON التي أرسلها العميل. يجب إزالة التسلسل منها لاستخدامها؛
- الأسطر 8-10: تتم عملية إزالة التسلسل. نحصل على قائمة بالمنتجات حيث يحتوي كل منتج على حقل [categorie=null]؛
- الأسطر 12-14: يتم إعادة تعيين الحقل [categorie] لجميع المنتجات في القائمة. وللقيام بذلك، يتم استخدام الحقل [idCategorie] للمنتج الذي تم تهيئته؛
- السطر 16: يتم إدراج المنتجات في قاعدة البيانات؛
- السطر 17: يتم تهيئة الكائن [response] بقائمة المنتجات؛
- السطران 18-19: الحالة التي تواجه فيها الطريقة استثناءً من الطبقة [DAO]. يتم تهيئة الاستجابة بـ [status=1000] (رمز الخطأ) [messages=e1.getMessages()]، أي يتم إرسال قائمة الأخطاء التي واجهت الجانب الخادم إلى العميل؛
- السطران 20-21: الحالة التي تواجه فيها الطريقة نوعًا آخر من الاستثناءات. يتم تهيئة الاستجابة بـ [status=1000] (رمز الخطأ) [messages=getErreursForException(e)] حيث [getErreursForException] هي طريقة خاصة بالفئة تعرض قائمة الأخطاء المرتبطة باستثناءات مكدس الاستثناءات لـ e، و [body=null]؛
- السطر 24: يتم إرجاع السلسلة jSON من الاستجابة؛
13.5.5.5. L'URL [/getAllProduits]
يتم معالجة URL [/getAllProduits] بالطريقة التالية:
@RequestMapping(value = "/getAllProduits", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllProduits() throws JsonProcessingException {
// الرد
Response<List<Produit>> response;
try {
response = new Response<List<Produit>>(0, null, application.getAllProduits());
} catch (DaoException e1) {
response = new Response<List<Produit>>(1003, e1.getErreurs(), null);
} catch (Exception e2) {
response = new Response<List<Produit>>(1003, getErreursForException(e2), null);
}
// الرد jSON
return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
- السطر 1: يتم طلب URL [/getAllProduits] باستخدام عملية [GET]. وتنتج jSON؛
- السطر 2: ترسل الطريقة بنفسها الرد jSON إلى العميل؛
- السطر 5: ترسل الطريقة السلسلة jSON من النوع [Response<List<Produit>>]؛
- السطر 7: يتم طلب المنتجات بدون فئتها؛
- الأسطر 8-12: في حالة حدوث خطأ، يتم تهيئة الرد برمز ورسائل خطأ؛
- السطر 14: يتم إرسال الرد jSON إلى العميل؛
13.5.5.6. Conclusion
لن نعرض الطرق الأخرى للمتحكم. فهي تشبه إحدى الطريقتين اللتين عرضناهما للتو.
13.5.6. فئة تنفيذ خدمة الويب / jSON
![]() |
فئة [Boot] هي فئة التنفيذ للمشروع:
package spring.webjson.boot;
import org.springframework.boot.SpringApplication;
import spring.webjson.server.config.AppConfig;
public class Boot {
public static void main(String[] args) {
SpringApplication.run(AppConfig.class, args);
}
}
- السطر 10: يتم تنفيذ الطريقة الثابتة [SpringApplication.run]. فئة [SpringApplication] هي فئة من مشروع [Spring Boot] (السطر 3). يتم تمرير معلمتين إليها:
- [AppConfig.class]: الفئة التي تهيئ التطبيق بأكمله؛
- [args]: أي معلمات يتم تمريرها إلى الطريقة [main] في السطر 9. لا يتم استخدام هذه المعلمة هنا؛
عند تنفيذ هذه الفئة، نحصل على السجلات التالية:
- الأسطر 17-19: بدء تشغيل خادم Tomcat الذي سيقوم بتنفيذ خدمة الويب / jSON؛
- الأسطر 25-33: إنشاء الطبقة [DAO]؛
- الأسطر 32-51: اكتشاف URL المعرضة؛
13.5.7. اختبارات خدمة الويب / jSON
لإجراء الاختبارات، نقوم بإنشاء قاعدة البيانات MySQL [dbintrospringdata] من البرنامج النصي SQL [dbintrospringdata.sql]:
![]() |
وبعد ذلك، نستخدم العميل [Advanced Rest Client] (انظر الفقرة 22.5) لاستعلام URL المعروضة بواسطة خدمة الويب / jSON (يجب تشغيل خدمة الويب / jSON).
![]() |
- في [1-3]، نطلب URL [/getAllCategories] عبر أمر HTTP GET؛
نحصل على الرد التالي:
![]() |
- في [1]، طلب HTTP من العميل؛
- في [2]، استجابة HTTP من الخادم؛
- إلى [3]، ويشير الحالة [200 OK] إلى أن الخادم قد عالج الطلب بشكل صحيح؛
- في [4]، استجابة الخادم jSON؛
الرد الكامل jSON هو كما يلي:
{"status":0,"messages":null,"body":[{"id":415,"version":0,"nom":"categorie0","produits":[{"id":1849,"version":0,"nom":"produit00","idCategorie":415,"prix":100.0,"description":"desc00"},{"id":1850,"version":0,"nom":"produit01","idCategorie":415,"prix":101.0,"description":"desc01"},{"id":1851,"version":0,"nom":"produit02","idCategorie":415,"prix":102.0,"description":"desc02"},{"id":1852,"version":0,"nom":"produit03","idCategorie":415,"prix":103.0,"description":"desc03"},{"id":1853,"version":0,"nom":"produit04","idCategorie":415,"prix":104.0,"description":"desc04"}]},{"id":416,"version":0,"nom":"categorie1","produits":[{"id":1856,"version":0,"nom":"produit12","idCategorie":416,"prix":112.0,"description":"desc12"},{"id":1857,"version":0,"nom":"produit13","idCategorie":416,"prix":113.0,"description":"desc13"},{"id":1858,"version":0,"nom":"produit14","idCategorie":416,"prix":114.0,"description":"desc14"},{"id":1854,"version":0,"nom":"produit10","idCategorie":416,"prix":110.0,"description":"desc10"},{"id":1855,"version":0,"nom":"produit11","idCategorie":416,"prix":111.0,"description":"desc11"}]}]}
- status:0 يعني أنه لم تحدث أخطاء من جانب الخادم؛
- messages: null تعني أنه لا توجد رسائل خطأ؛
- body: هو نص الرد، وهنا قائمة الفئات مع منتجاتها. هناك فئتان تحتوي كل منهما على 5 منتجات؛
سنقوم بإضافة المنتج [produit15] إلى الفئة [categorie1]. وللقيام بذلك، سنستخدم URL [/addCategories] الذي يحمل الرمز التالي:
@RequestMapping(value = "/addCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addCategories(HttpServletRequest request) throws JsonProcessingException {
Response<List<Categorie>> response;
ObjectMapper mapper = context.getBean(ObjectMapper.class);
// يتم الاحتفاظ بالفئات
try {
// استرداد القيمة المنشورة
String body = CharStreams.toString(request.getReader());
mapper.setFilters(jsonFilterCategorieWithProduits);
List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
});
// إعادة إنشاء الارتباط بين المنتجات والفئات
for (Categorie categorie : categories) {
Set<Produit> produits = categorie.getProduits();
if (produits != null) {
for (Produit produit : categorie.getProduits()) {
produit.setCategorie(categorie);
}
}
}
// يتم الاحتفاظ بالفئات
application.addCategories(categories);
response = new Response<List<Categorie>>(0, null, categories);
} catch (Exception e) {
response = new Response<List<Categorie>>(1004, getErreursForException(e), null);
}
// الرد jSON
return mapper.writeValueAsString(response);
}
- السطر 1: يجب على العميل إنشاء POST ويجب أن تكون القيمة المنشورة سلسلة jSON؛
- الأسطر 9-12: يجب أن تكون القيمة المنشورة قائمة بالفئات مع المنتجات المرتبطة بها؛
سنقوم بإنشاء فئة [categorie2] مع منتج [produit21]. وبالتالي، فإن السلسلة jSON المراد إرسالها هي التالية:
[{"id":null,"version":0,"nom":"categorie2","produits":[{"id":null,"version":0,"nom":"produit21","idCategorie":null,"prix":111.0,"description":"desc21"}]}]
يتم إجراء الطلب إلى خدمة الويب / jSON بالطريقة التالية:
![]() |
- في [1]، URL المطلوب؛
- في [2]، يتم طلبها عبر عملية POST؛
- في [3]، تم إرسال السلسلة jSON؛
- في [4]، يتم إخطار الخادم بأنه سيتم إرسال jSON إليه؛
رد الخادم هو التالي:
![]() |
- في [1]، نرى أن الفئة والمنتج لديهما الآن مفتاح أساسي، مما يدل على أنهما قد تم إدخالهما على الأرجح في قاعدة البيانات. سنقوم بالتحقق من ذلك باستخدام URL [/getCategorieByNameWithProduits/categorie2]:
![]() |
نحصل على النتيجة التالية:
![]() |
لقد حصلنا بالفعل على الفئة [categorie2] مع منتجها الوحيد [produit21]. يمكننا أيضًا طلب المنتج فقط. لنستخدم لذلك URL [/getProduitByIdWithoutCategorie/1859]:
![]() |
نحصل على النتيجة التالية:
![]() |
يمكن إجراء جميع العمليات [GET] في متصفح بسيط:
![]() |
ندعو القارئ إلى تجربة عمليات URL الأخرى الخاصة بخدمة الويب / json.
13.6. عميل مبرمج لخدمة الويب / jSON
الآن بعد أن أصبحت قاعدة [dbintrospringdata] متاحة على الويب، سنقوم بكتابة تطبيق يستفيد منها. سيكون لدينا عندئذٍ بنية العميل / الخادم التالية:
![]() |
سيكون للتطبيق العميل طبقتان:
- طبقة [DAO] [2] للتواصل مع تطبيق الويب / jSON الذي يعرض قاعدة البيانات؛
- طبقة اختبار JUnit [1] للتحقق من أن العميل والخادم يؤديان عملهما بشكل صحيح؛
13.6.1. مشروع Eclipse
مشروع Eclipse الخاص بالعميل هو التالي:
![]() |
- المجلد [src/main/java] ينفذ الطبقة [DAO]؛
- المجلد [src/test/java] ينفذ الاختبارات JUnit؛
13.6.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>istia.st.webjson</groupId>
<artifactId>intro-client-webjson-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>Client console du serveur web / jSON</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- مكتبة jSON المستخدمة بواسطة 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>
<!-- مكون يستخدمه 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>
<scope>test</scope>
</dependency>
<!-- مكتبة السجلات -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- اختبار Spring Boot -->
<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>
<!-- المكونات الإضافية -->
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
<name>intro-client-webjson-01</name>
</project>
- الأسطر 14-18: مشروع Maven الأصلي [spring-boot-starter-parent] الذي يسمح لنا بتحديد عدد من التبعيات دون تحديد إصداراتها، حيث يتم تحديد الإصدارات في المشروع الأصلي؛
- الأسطر 22-25: على الرغم من أننا لا نكتب تطبيق ويب، إلا أننا نحتاج إلى التبعية [spring-web] التي تجلب معها الفئة [RestTemplate] التي تسمح بالتفاعل بسهولة مع تطبيق ويب / jSON؛
- الأسطر 27-34: مكتبة jSON؛
- الأسطر 36-39: تبعية ستسمح لنا بتعيين timeout لطلبات HTTP من العميل. timeout هو الحد الأقصى لوقت انتظار استجابة الخادم. بعد هذا الوقت، يبلغ العميل عن خطأ timeout بإلقاء استثناء؛
- الأسطر 41-46: مكتبة Google Guava المستخدمة في الاختبار JUnit. لهذا السبب، قمنا بتحديد نطاقها إلى [test] (السطر 45). وهذا يعني أن هذه التبعية لا يتم تضمينها إلا عند تنفيذ أكواد الفرع [src/test/java]؛
- الأسطر 48-51: مكتبة السجلات؛
- الأسطر 52-63: التبعية الخاصة باختبارات JUnit. وهي تجلب على وجه الخصوص المكتبة JUnit 4 اللازمة للاختبارات. تحتوي هذه التبعيات على السمة [<scope>test</scope>] التي تشير إلى أنها ضرورية فقط لمرحلة الاختبارات. وهي غير مدرجة في الأرشيف النهائي للمشروع؛
13.6.3. تنفيذ الطبقة [DAO]
![]() |
![]() |
- تحتوي الحزمة [spring.client.config] على تكوين Spring للطبقة [DAO]؛
- يحتوي الحزمة [spring.client.dao] على تنفيذ الطبقة [DAO]؛
- تحتوي الحزمة [spring.client.entities] على الكائنات المتبادلة مع خدمة الويب / jSON؛
13.6.3.1. Configuration
![]() |
تقوم الفئة [DaoConfig] بإجراء تكوين Spring للطبقة [DAO]. وفيما يلي شفرتها:
package spring.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
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;
@ComponentScan({ "spring.client.dao" })
public class DaoConfig {
// الثوابت
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(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategorieWithProduits")
public ObjectMapper jsonMapperCategorieWithProduits() {
// أداة التعيين jSON
ObjectMapper mapper = new ObjectMapper();
// مرشحات
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// النتيجة
return mapper;
}
@Bean(name = "jsonMapperProduitWithCategorie")
public ObjectMapper jsonMapperProduitWithCategorie() {
// مُخطِط jSON
ObjectMapper mapper = new ObjectMapper();
// مرشحات
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// النتيجة
return mapper;
}
@Bean(name = "jsonMapperCategorieWithoutProduits")
public ObjectMapper jsonMapperCategorieWithoutProduits() {
// مُخطِط jSON
ObjectMapper mapper = new ObjectMapper();
// مرشحات
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// النتيجة
return mapper;
}
@Bean(name = "jsonMapperProduitWithoutCategorie")
public ObjectMapper jsonMapperProduitWithoutCategorie() {
// مُخطِط jSON
ObjectMapper mapper = new ObjectMapper();
// مرشحات
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// النتيجة
return mapper;
}
}
- السطر 13: الفئة هي فئة تكوين Spring - يجب البحث عن مكونات Spring في الحزمة [spring.client.dao]؛
- السطر 17: يتم تعيين timeout لمدة ثانية واحدة (1000 مللي ثانية)؛
- الأسطر 32-35: الفول الذي يعرض هذه القيمة؛
- السطر 18: URL لخدمة الويب / jSON؛
- الأسطر 37-40: الكائن الذي يعرض هذه القيمة؛
- الأسطر 20-30: تكوين الفئة [RestTemplate] التي تضمن التبادل مع خدمة الويب / jSON. عندما لا يتعين تكوينها، يمكن استخدامها في الكود ببساطة عن طريق [new RestTemplate()]. هنا، نريد تعيين timeout للتبادل مع خدمة الويب / jSON. يتم تمرير bean [timeout] في السطر 36 كمعلمة لطريقة [restTemplate] في السطر 24؛
- السطر 23: المكون [HttpComponentsClientHttpRequestFactory] هو المكون الذي يسمح لنا بتحديد timeout للتبادل (السطران 29-30)؛
- السطر 24: تم إنشاء الفئة [RestTemplate] باستخدام هذا المكون. ونظرًا لأنها تعتمد عليه للتواصل مع خدمة الويب / jSON، فسيتم إخضاع التبادلات بالفعل لـ timeout؛
- سيتبادل العميل والخادم أسطر نصية. يتولى المحول تحويل كائن إلى نص والعكس بالعكس، أي تحويل نص إلى كائن. قد يكون هناك عدة محولات مرتبطة بالفئة [RestTemplate]، ويعتمد اختيار المحول في لحظة معينة على الرؤوس HTTP المرسلة من الخادم. هنا، لن يكون لدينا أي محول. لذلك، لن يحاول المكون [RestTemplate] تحويل العنصرين التاليين بأي شكل من الأشكال:
- النص المنشور؛
- النص المستلم كرد؛
ستكون هذه النصوص سلاسل jSON والتي سيتم تركها كما هي بواسطة المكون [RestTemplate]. نحن كمطورين، سنقوم بعمليات التسلسل / إزالة التسلسل jSON اللازمة. وذلك لأن المرشحات التي سيتم تطبيقها على القيمة المرسلة والرد المستلم قد تكون مختلفة، وتُظهر التجربة أنه من الأسهل إدارتها بنفسك بدلاً من محاولة تكوين المكون [RestTemplate] بحيث يستخدم المحول الصحيح jSON؛
- الأسطر 42-92: تحدد مرشحات jSON. وهي نفس المرشحات الموجودة في الخادم والموضحة في الفقرة 13.5.3.1؛
- الأسطر 43-46: مُعِد خرائط jSON بدون مرشحات؛
- الأسطر 64-68: مُعِدّ قوالب jSON لإنشاء فئة بدون منتجاتها؛
- الأسطر 48-58: مخطط jSON للحصول على فئة مع منتجاتها؛
- الأسطر 83-92: مخطط jSON للحصول على منتج بدون فئته؛
- الأسطر 60-70: مخطط jSON للحصول على منتج مع فئته؛
ستكون جميع هذه الفاصوليا متاحة في أكواد الطبقة [DAO] وكذلك في اختبار Junit.
13.6.3.2. الكيانات
![]() |
الكيانات التي تعالجها الطبقة [DAO] هي تلك التي تتبادلها مع خدمة الويب / jSON. وهي المقالات والمنتجات. على جانب الخادم، كانت هذه الكيانات تحتوي على تعليقات توضيحية خاصة بالاستمرارية JPA. هنا، تمت إزالة هذه التعليقات التوضيحية. نعيد عرض كود الكيانات للتذكير:
[AbstractEntity]
package spring.client.entities;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
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();
}
// التوقيع jSON
public String toString() {
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.writeValueAsString(this);
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
// مُستردات ومُعيّنات
...
}
[Categorie]
package spring.client.entities;
import java.util.HashSet;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractEntity {
// الخصائص
private String nom;
// المنتجات ذات الصلة
public Set<Produit> produits = new HashSet<Produit>();
// المنشئات
public Categorie() {
}
public Categorie(String nom) {
this.nom = nom;
}
// الأساليب
public void addProduit(Produit produit) {
// إضافة المنتج
produits.add(produit);
// تحديد فئته
produit.setCategorie(this);
}
// مُستردات ومُعيّنات
...
}
[Produit]
package spring.webjson.client.entities;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {
// الاسم
private String nom;
// رقم الفئة
private Long idCategorie;
// السعر
private double prix;
// الوصف
private String description;
// الفئة
private Categorie categorie;
// الشركات المصنعة
public Produit() {
}
public Produit(String nom, double prix, String description) {
this.nom = nom;
this.prix = prix;
this.description = description;
}
// المُستقبلات والمُعيّنات
...
}
13.6.3.3. الفئة [DaoException]
![]() |
عندما تواجه الطبقة [DAO] خطأً، فإنها ستطلق استثناءً من النوع [DaoException]. هذه الفئة هي تلك المستخدمة على جانب الخادم والموصوفة في الفقرة 11.3.7.
13.6.3.4. واجهة الطبقة [DAO]
![]() |
تقدم الطبقة [DAO] واجهة [IDao] الموصوفة في الفقرة 11.3.7.
package spring.client.dao;
import java.util.List;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
public interface IDao {
// إدراج قائمة المنتجات
public List<Produit> addProduits(List<Produit> produits);
// حذف جميع المنتجات
public void deleteAllProduits();
// تحديث قائمة المنتجات
public List<Produit> updateProduits(List<Produit> produits);
// الحصول على جميع المنتجات
public List<Produit> getAllProduits();
// إدراج قائمة بالفئات
public List<Categorie> addCategories(List<Categorie> categories);
// حذف جميع الفئات
public void deleteAllCategories();
// تحديث قائمة الفئات
public List<Categorie> updateCategories(List<Categorie> categories);
// الحصول على جميع الفئات
public List<Categorie> getAllCategories();
// منتج معين
public Produit getProduitByIdWithCategorie(Long idProduit);
public Produit getProduitByIdWithoutCategorie(Long idProduit);
public Produit getProduitByNameWithCategorie(String nom);
public Produit getProduitByNameWithoutCategorie(String nom);
// فئة معينة
public Categorie getCategorieByIdWithProduits(Long idCategorie);
public Categorie getCategorieByIdWithoutProduits(Long idCategorie);
public Categorie getCategorieByNameWithProduits(String nom);
public Categorie getCategorieByNameWithoutProduits(String nom);
}
13.6.3.5. استجابة خدمة الويب / jSON
![]() |
لقد رأينا أن جميع URL لخدمة الويب / jSON تعطي نوعًا [Response] المحدد في الفقرة 13.5.5.3. نعيد هنا هذه الفئة:
package spring.client.dao;
import java.util.List;
public class Response<T> {
// ----------------- الخصائص
// حالة العملية
private int status;
// رسائل الخطأ المحتملة
private List<String> messages;
// نص الرد
private T body;
// المنشئات
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// مُستردات ومُعيّنات
...
}
13.6.3.6. تنفيذ التبادلات مع خدمة الويب / jSON
![]() |
تنفذ الفئة [AbstractDao] التبادلات مع خدمة الويب / jSON:
package spring.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.web.client.RestTemplate;
public abstract class AbstractDao {
// البيانات
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// الاستعلام العام
protected String getResponse(String url, String jsonPost) {
// url: URL للاتصال
// jsonPost: القيمة jSON المطلوب إرسالها
try {
// تنفيذ الطلب
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 DaoException(20, e1);
} catch (RuntimeException e2) {
throw new DaoException(21, e2);
}
}
}
- السطران 15-16: إدخال المكون [RestTemplate] الذي يضمن الاتصال بالخادم؛
- السطران 17-18: إدخال المكون URL لخدمة الويب / jSON؛
تم تجميع تنفيذ طرق الاتصال بالخادم في الطريقة [getResponse]:
- السطر 21: تتلقى الطريقة معلمتين:
- [url]: URL المطلوب؛
- [jsonPost]: السلسلة jSON المراد نشرها، وإلا null. إذا كان [jsonPost==null]، يتم إجراء طلب URL باستخدام GET، وإلا باستخدام POST؛
- السطر 38: التعليمات التي ترسل الطلب إلى الخادم وتتلقى الرد. يوفر المكون [RestTemplate] عددًا كبيرًا من طرق التبادل مع الخادم. وقد اخترنا هنا الطريقة [exchange]، ولكن هناك طرق أخرى؛
- الأسطر 27-36: علينا إنشاء الطلب من النوع [RequestEntity]. ويختلف هذا الطلب باختلاف استخدام GET أو POST لإجراء الطلب؛
- السطور 30-31: الاستعلام الخاص بـ GET. توفر الفئة [RequestEntity] طرقًا ثابتة لإنشاء الاستعلامات GET و POST و HEAD،... تسمح الطريقة [RequestEntity.get] بإنشاء طلب GET من خلال ربط الطرق المختلفة التي تبني هذا الطلب:
- تقبل الطريقة [RequestEntity.get] كمعلمة الهدف URL في شكل مثيل URI،
- تسمح الطريقة [accept] بتحديد عناصر الرأس HTTP [Accept]. هنا، نشير إلى أننا نقبل النوع [application/json] الذي سيرسله الخادم؛
- تستخدم الطريقة [build] هذه المعلومات المختلفة لإنشاء نوع [RequestEntity] للطلب؛
- السطران 34-35: الطلب لـ POST. تسمح الطريقة [RequestEntity.post] بإنشاء طلب POST من خلال ربط الطرق المختلفة التي تبني هذا الطلب:
- تقبل الطريقة [RequestEntity.post] كمعلمة الهدف URL في شكل مثيل URI،
- تحدد الطريقة [header] رأسًا HTTP. هنا نرسل إلى الخادم رأس [Content-Type: application/json] لإعلامه بأن القيمة المرسلة ستصل إليه في شكل سلسلة jSON؛
- تسمح الطريقة [accept] بالإشارة إلى أننا نقبل النوع [application/json] الذي سيرسله الخادم؛
- تحدد الطريقة [body] القيمة المرسلة. وهي المعلمة الرابعة للطريقة العامة [getResponse] (السطر 1)؛
- السطر 38: تُرجع الطريقة [RestTemplate].exchange نوعًا [ResponseEntity<String>] الذي يغلف كامل استجابة الخادم: رؤوس HTTP ونص الوثيقة. تسمح الطريقة [ResponseEntity].getBody() بالحصول على هذا النص الذي يمثل استجابة الخادم، وهو هنا سلسلة من الأحرف؛
13.6.3.7. تنفيذ واجهة [IDao]
![]() |
تنفذ الفئة [Dao] الواجهة [IDao]:
package spring.client.dao;
import java.io.IOException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
@Component
public class Dao extends AbstractDao implements IDao {
@Autowired
private ApplicationContext context;
// تصفية jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
@Override
public List<Produit> addProduits(List<Produit> produits) {
// ----------- إضافة منتجات (بدون فئتها)
...
}
- السطر 17: الفئة [Dao] هي مكون Spring يمكننا من خلاله حقن مكونات Spring أخرى؛
- السطر 18: الفئة [Dao] تمتد الفئة [AbstractDao] التي رأيناها للتو وتنفذ الواجهة [IDao]؛
- السطران 20-21: يتم حقن سياق Spring للوصول إلى حبوبه؛
- الأسطر 24-38: إدراج المُعَدِّلات jSON المُعرَّفة في الفئة [AppConfig] المُقدَّمة في الفقرة 13.6.2؛
تتبع جميع تطبيقات الطرق المختلفة لواجهة [IDao] نفس النمط. سنعرض طريقتين، إحداهما تعتمد على عملية [POST]، والأخرى على عملية [GET].
مثال على [GET]: [getCategorieByNameWithProduits]
@Override
public Categorie getCategorieByNameWithProduits(String nom) {
// ----------- الحصول على فئة محددة بالاسم، مع منتجاتها
try {
// الاستعلام
Response<Categorie> response = jsonMapperCategorieWithProduits.readValue(
getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null),
new TypeReference<Response<Categorie>>() {
});
// خطأ؟
if (response.getStatus() != 0) {
// يتم إلقاء استثناء واحد
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// يتم إرجاع جوهر استجابة الخادم
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (RuntimeException | IOException e2) {
throw new DaoException(113, e2);
}
}
- السطر 7: يتم استدعاء الطريقة [getResponse] من الفئة الأصلية. هذه الطريقة هي التي تضمن التبادل مع خدمة الويب / jSON. معلماتها هي كما يلي:
getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null)
- (تابع)
- URL للخدمة المستفسر عنها [/getCategorieByNameWithProduits/nom]؛
- القيمة المرسلة. لا توجد قيمة هنا؛
تُرجع الطريقة [getResponse] نوع String وهو الرد jSON المرسل من الخادم. يتم فك تسلسل هذا الرد jSON بالطريقة التالية:
jsonMapperCategorieWithProduits.readValue(
jsonResponse,
new TypeReference<Response<Categorie>>() {
});
لأن السلسلة jSON هي تسلسل لنوع [Response<Categorie>]؛
- السطور 11-17: يتم اختبار حالة الرد. إذا كانت الحالة مختلفة عن 0، فهذا يعني أن هناك خطأ من جانب الخادم. عندئذ يتم إلقاء استثناء (السطر 13)، مع استرجاع المعلومات الواردة في الرد (الحالة وقائمة رسائل الخطأ)؛
- السطر 16: إذا لم يكن هناك خطأ من جانب الخادم، يتم إرجاع نص النوع [Response<Categorie>]، أي الفئة المطلوبة؛
- السطران 18-19: يتم التعامل مع الاستثناء الذي تم إطلاقه في السطر 16؛
- السطور 20-22: معالجة جميع الاستثناءات الأخرى؛
مثال على [POST]: [addCategories]
@Override
public List<Categorie> addCategories(List<Categorie> categories) {
// ----------- إضافة فئات (مع منتجاتها)
try {
// طلب
Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories)),
new TypeReference<Response<List<Categorie>>>() {
});
// خطأ؟
if (response.getStatus() != 0) {
// يتم إلقاء استثناء واحد
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// يتم عرض جوهر استجابة الخادم
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (RuntimeException | IOException e2) {
throw new DaoException(104, e2);
}
}
- السطر 2: تُستخدم الطريقة [addCategories] لتخزين الفئات التي تم تمريرها كمعلمات في قاعدة البيانات. وهي تُثري هذه الفئات نفسها بمفاتيحها الأساسية. إذا تم تمرير الفئات مع منتجات، يتم تخزين هذه المنتجات أيضًا؛
- السطر 7: يتم استدعاء الطريقة [getResponse] من الأصل لإجراء التبادلات مع خدمة الويب / jSON؛
- المعلمة الأولى هي URL [/addCategories]؛
- المعلمة الثانية هي القيمة المرسلة، وهي هنا قائمة الفئات المطلوب الاحتفاظ بها؛
getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories))
ثم يتم إزالة التسلسل من السلسلة jSON التي تم الحصول عليها للحصول على النوع المتوقع [Response<List<Categorie>]:
Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
jsonResponse,
new TypeReference<Response<List<Categorie>>>() {
});
- الأسطر 11-17: إدارة استجابة الخادم (خطأ أم لا)؛
- الأسطر 20-22: إدارة الاستثناءات؛
تتبع جميع الطرق الأخرى نمط الطريقتين المقدمتين.
13.6.4. اختبار JUnit
لنعد إلى بنية العميل/الخادم قيد الإنشاء:
![]() |
لقد أنشأنا طبقة [DAO] [2] بنفس واجهة الطبقة [DAO] [4]. لاختبار الطبقة [DAO] [2]، يمكننا إذن استخدام الاختبار JUnit الذي استُخدم لاختبار الطبقة [DAO] [4]. للتذكير، هذا الاختبار هو التالي:
![]() |
package spring.client.junit;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import spring.client.config.DaoConfig;
import spring.client.dao.DaoException;
import spring.client.dao.IDao;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {
// طبقة [DAO]
@Autowired
private IDao dao;
// مرشحات jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
@Before
public void cleanAndFill() {
// يتم تنظيف قاعدة البيانات قبل كل اختبار
log("Vidage de la base de données", 1);
// يتم إفراغ الجدول [CATEGORIES] - وبالتالي سيتم إفراغ الجدول [PRODUITS]
dao.deleteAllCategories();
// --------------------------------------------------------------------------------------
log("Remplissage de la base", 1);
// يتم ملء الجداول
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < 2; i++) {
Categorie categorie = new Categorie(String.format("categorie%d", i));
for (int j = 0; j < 5; j++) {
categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
String.format("desc%d%d", i, j)));
}
categories.add(categorie);
}
// إضافة الفئة - سيتم إدراج المنتجات أيضًا بشكل متسلسل
categories = dao.addCategories(categories);
}
@Test
public void showDataBase() throws BeansException, JsonProcessingException {
// قائمة الفئات
log("Liste des catégories", 2);
List<Categorie> categories = dao.getAllCategories();
affiche(categories, jsonMapperCategorieWithoutProduits);
// قائمة المنتجات
log("Liste des produits", 2);
List<Produit> produits = dao.getAllProduits();
affiche(produits, jsonMapperProduitWithoutCategorie);
// بعض عمليات التحقق
Assert.assertEquals(2, categories.size());
Assert.assertEquals(10, produits.size());
Categorie categorie = findCategorieByName("categorie0", categories);
Assert.assertNotNull(categorie);
Produit produit = findProduitByName("produit03", produits);
Assert.assertNotNull(produit);
Long idCategorie = produit.getIdCategorie();
Assert.assertEquals(categorie.getId(), idCategorie);
}
@Test
public void getCategorieByNameWithProduits() {
log("getCategorieByNameWithProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Assert.assertNotNull(categorie1);
Assert.assertEquals(5, categorie1.getProduits().size());
}
@Test
public void getCategorieByNameWithoutProduits() {
log("getCategorieByNameWithoutProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithoutProduits("categorie1");
Assert.assertNotNull(categorie1);
Assert.assertEquals("categorie1", categorie1.getNom());
}
@Test
public void getCategorieByIdWithProduits() {
log("getCategorieByIdWithProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Categorie categorie2 = dao.getCategorieByIdWithProduits(categorie1.getId());
Assert.assertNotNull(categorie2);
Assert.assertEquals(categorie1.getId(), categorie2.getId());
Assert.assertEquals(categorie1.getNom(), categorie2.getNom());
}
@Test
public void getCategorieByIdWithoutProduits() {
log("getCategorieByIdWithoutProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Categorie categorie2 = dao.getCategorieByIdWithoutProduits(categorie1.getId());
Assert.assertNotNull(categorie2);
Assert.assertEquals(categorie1.getNom(), categorie2.getNom());
}
@Test
public void getProduitByNameWithCategorie() {
log("getProduitByNameWithCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Assert.assertNotNull(produit);
Assert.assertNotNull(produit.getCategorie());
}
@Test
public void getProduitByNameWithoutCategorie() {
log("getProduitByNameWithoutCategorie", 1);
Produit produit = dao.getProduitByNameWithoutCategorie("produit03");
Assert.assertNotNull(produit);
Assert.assertEquals("produit03", produit.getNom());
}
@Test
public void getProduitByIdWithCategorie() {
log("getProduitByNameWithCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Produit produit2 = dao.getProduitByIdWithCategorie(produit.getId());
Assert.assertNotNull(produit2);
Assert.assertEquals(produit2.getNom(), produit.getNom());
Assert.assertEquals(produit2.getId(), produit.getId());
Assert.assertEquals(produit.getCategorie().getId(), produit2.getCategorie().getId());
}
@Test
public void getProduitByIdWithoutCategorie() {
log("getProduitByIdWithoutCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Produit produit2 = dao.getProduitByIdWithoutCategorie(produit.getId());
Assert.assertNotNull(produit2);
Assert.assertEquals(produit2.getNom(), produit.getNom());
Assert.assertEquals(produit2.getId(), produit.getId());
}
@Test
public void doInsertsInTransaction() {
log("Ajout d'une catégorie [cat1] avec deux produits de même nom", 1);
// نقوم بالإدراج
Categorie categorie = new Categorie("cat1");
categorie.addProduit(new Produit("x", 1.0, ""));
categorie.addProduit(new Produit("x", 1.0, ""));
// إضافة الفئة - سيتم إدراج المنتجات أيضًا بشكل متسلسل
try {
categorie = dao.addCategories(Lists.newArrayList(categorie)).get(0);
} catch (DaoException e) {
show("Les erreurs suivantes se sont produites :", e.getErreurs());
}
// عمليات التحقق
List<Categorie> categories = dao.getAllCategories();
Assert.assertEquals(2, categories.size());
List<Produit> produits = dao.getAllProduits();
Assert.assertEquals(10, produits.size());
}
@Test
public void updateDataBase() {
log("Mise à jour du prix des produits de [categorie1]", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Categorie categorie1Saved = dao.getCategorieByNameWithProduits("categorie1");
Set<Produit> produits = categorie1.getProduits();
for (Produit produit : produits) {
produit.setPrix(1.1 * produit.getPrix());
}
List<Produit> produits2 = Lists.newArrayList(produits);
produits2 = dao.updateProduits(produits2);
// عمليات التحقق
List<Produit> produitsSaved = Lists.newArrayList(categorie1Saved.getProduits());
for (Produit produit2 : produits2) {
Produit produit = findProduitByName(produit2.getNom(), produitsSaved);
Assert.assertEquals(produit2.getPrix(), produit.getPrix() * 1.1, 1e-6);
}
}
@Test
public void addProduits() throws BeansException, JsonProcessingException {
log("Ajout de deux produits de catégorie [categorie0]", 1);
Categorie categorie0 = dao.getCategorieByNameWithoutProduits("categorie0");
Long idCategorie = categorie0.getId();
Produit p1 = new Produit("x", 1, "");
p1.setIdCategorie(idCategorie);
p1.setCategorie(categorie0);
Produit p2 = new Produit("y", 1, "");
p2.setIdCategorie(idCategorie);
p2.setCategorie(categorie0);
List<Produit> produits = new ArrayList<Produit>();
produits.add(p1);
produits.add(p2);
produits = dao.addProduits(produits);
// التحقق
affiche(produits, jsonMapperProduitWithoutCategorie);
}
// -------------- طرق خاصة
private Produit findProduitByName(String nom, List<Produit> produits) {
for (Produit produit : produits) {
if (produit.getNom().equals(nom)) {
return produit;
}
}
return null;
}
private Categorie findCategorieByName(String nom, List<Categorie> categories) {
for (Categorie categorie : categories) {
if (categorie.getNom().equals(nom)) {
return categorie;
}
}
return null;
}
// عرض عنصر من النوع T
static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
System.out.println(jsonMapper.writeValueAsString(element));
}
// عرض قائمة بعناصر من النوع T
static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
for (T element : elements) {
affiche(element, jsonMapper);
}
}
private static void log(String message, int mode) {
// عرض رسالة
String toPrint = null;
switch (mode) {
case 1:
toPrint = String.format("%s --------------------------------", message);
break;
case 2:
toPrint = String.format("-- %s", message);
break;
}
System.out.println(toPrint);
}
private static void show(String title, List<String> messages) {
// العنوان
System.out.println(String.format("%s : ", title));
// الرسائل
for (String message : messages) {
System.out.println(String.format("- %s", message));
}
}
}
تم تنفيذه بنجاح وأعطى النتائج التالية على وحدة التحكم:
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Ajout de deux produits de catégorie [categorie0] --------------------------------
{"id":6285,"version":0,"nom":"x","idCategorie":1319,"prix":1.0,"description":""}
{"id":6286,"version":0,"nom":"y","idCategorie":1319,"prix":1.0,"description":""}
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Mise à jour du prix des produits de [categorie1] --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByIdWithoutProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithoutCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByNameWithProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByNameWithoutProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByIdWithoutCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
-- Liste des catégories
{"id":1337,"version":0,"nom":"categorie0"}
{"id":1338,"version":0,"nom":"categorie1"}
-- Liste des produits
{"id":6367,"version":0,"nom":"produit00","idCategorie":1337,"prix":100.0,"description":"desc00"}
{"id":6368,"version":0,"nom":"produit01","idCategorie":1337,"prix":101.0,"description":"desc01"}
{"id":6369,"version":0,"nom":"produit02","idCategorie":1337,"prix":102.0,"description":"desc02"}
{"id":6370,"version":0,"nom":"produit03","idCategorie":1337,"prix":103.0,"description":"desc03"}
{"id":6371,"version":0,"nom":"produit04","idCategorie":1337,"prix":104.0,"description":"desc04"}
{"id":6372,"version":0,"nom":"produit10","idCategorie":1338,"prix":110.0,"description":"desc10"}
{"id":6373,"version":0,"nom":"produit11","idCategorie":1338,"prix":111.0,"description":"desc11"}
{"id":6374,"version":0,"nom":"produit12","idCategorie":1338,"prix":112.0,"description":"desc12"}
{"id":6375,"version":0,"nom":"produit13","idCategorie":1338,"prix":113.0,"description":"desc13"}
{"id":6376,"version":0,"nom":"produit14","idCategorie":1338,"prix":114.0,"description":"desc14"}
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByIdWithProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Ajout d'une catégorie [cat1] avec deux produits de même nom --------------------------------
Les erreurs suivantes se sont produites :
- org.hibernate.exception.ConstraintViolationException: could not execute statement
- could not execute statement
- Duplicate entry 'x' for key 'NOM'
11:24:37.650 [Thread-1] INFO o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@f8c1ddd: startup date [Fri Nov 20 11:24:34 CET 2015]; root of context hierarchy



























































