2. خادم Spring 4
![]() |
في البنية المذكورة أعلاه، ننتقل الآن إلى إنشاء خدمة الويب / JSON المبنية باستخدام إطار عمل Spring 4. سنقوم بكتابتها على عدة مراحل:
- أولاً الطبقات [métier] و [DAO] (كائنات الوصول إلى البيانات). سنستخدم هنا Spring Data؛
- ثم خدمة الويب JSON بدون مصادقة. سنستخدم هنا Spring MVC؛
- ثم سنضيف جزء المصادقة باستخدام Spring Security.
نبدأ بتوضيح بنية قاعدة البيانات التي تدعم التطبيق.
2.1. قاعدة البيانات
![]() |
قاعدة البيانات التي سنطلق عليها فيما بعد اسم [dbrdvmedecins] هي قاعدة بيانات MySQL5 تحتوي على الجداول التالية:
![]() |
تتم إدارة المواعيد من خلال الجداول التالية:
- [medecins]: تحتوي على قائمة أطباء العيادة؛
- [clients]: تحتوي على قائمة المرضى في العيادة؛
- [creneaux]: تحتوي على الفترات الزمنية المتاحة لكل طبيب؛
- [rv]: تحتوي على قائمة مواعيد الأطباء.
الجداول [roles] و [users] و [users_roles] هي جداول مرتبطة بعملية المصادقة. في البداية، لن نتطرق إليها.
العلاقات بين الجداول التي تدير المواعيد هي كما يلي:
![]() |
- كل فترة زمنية تخص طبيبًا واحدًا – ويكون لكل طبيب فترة زمنية واحدة أو أكثر؛
- يجمع الموعد بين عميل وطبيب عبر فترة زمنية مخصصة لهذا الأخير؛
- كل عميل لديه صفر أو أكثر من المواعيد؛
- يرتبط كل فترة زمنية بموعد واحد أو أكثر (في أيام مختلفة).
2.1.1. الجدول [MEDECINS]
تحتوي على معلومات عن الأطباء الذين تديرهم التطبيق [RdvMedecins].
![]() | ![]() |
- ID: الرقم الذي يحدد هوية الطبيب - المفتاح الأساسي للجدول
- VERSION: الرقم الذي يحدد إصدار السطر في الجدول. يتم زيادة هذا الرقم بمقدار 1 في كل مرة يتم فيها إجراء تعديل على السطر.
- NOM: اسم الطبيب
- PRENOM: اسمه الأول
- TITRE: لقبه (الآنسة، السيدة، السيد)
2.1.2. الجدول [CLIENTS]
يتم تسجيل مرضى الأطباء المختلفين في الجدول [CLIENTS]:
![]() | ![]() |
- ID: الرقم التعريفي للعميل - المفتاح الأساسي للجدول
- VERSION: الرقم الذي يحدد إصدار السطر في الجدول. يتم زيادة هذا الرقم بمقدار 1 في كل مرة يتم فيها إجراء تعديل على السطر.
- NOM: اسم العميل
- PRENOM: اسمه الأول
- TITRE: لقبه (الآنسة، السيدة، السيد)
2.1.3. الجدول [CRENEAUX]
تسرد هذه الجدول الفترات الزمنية التي يمكن فيها استخدام RV:
![]() |
![]() |
- ID: الرقم الذي يحدد الفترة الزمنية - المفتاح الأساسي للجدول (السطر 8)
- VERSION: الرقم الذي يحدد إصدار السطر في الجدول. يتم زيادة هذا الرقم بمقدار 1 في كل مرة يتم فيها إجراء تعديل على السطر.
- ID_MEDECIN: الرقم الذي يحدد الطبيب الذي ينتمي إليه هذا الموعد – مفتاح خارجي في العمود MEDECINS (ID).
- HDEBUT: وقت بدء الفترة الزمنية
- MDEBUT: الدقائق الأولى للفترة
- HFIN: ساعة انتهاء الفترة الزمنية
- MFIN: الدقائق التي تنتهي فيها الفترة الزمنية
يشير السطر الثاني من الجدول [CRENEAUX] (انظر [1] أعلاه)، على سبيل المثال، إلى أن الفترة رقم 2 تبدأ في الساعة 8:20 وتنتهي في الساعة 8:40، وتخص الطبيبة رقم 1 (السيدة ماري PELISSIER).
2.1.4. الجدول [RV]
تسرد المواعيد المحددة لكل طبيب:
![]() |
- ID: الرقم الذي يحدد السجل RV بشكل فريد – المفتاح الأساسي
- JOUR: اليوم الخاص بـ RV
- ID_CRENEAU: الفترة الزمنية لـ RV - مفتاح خارجي في الحقل [ID] من الجدول [CRENEAUX] – يحدد في آن واحد الفترة الزمنية والطبيب المعني.
- ID_CLIENT: رقم العميل الذي تم الحجز لصالحه – مفتاح خارجي في الحقل [ID] في الجدول [CLIENTS]
تحتوي هذه الجدولة على قيد « » يضمن تفرد قيم الأعمدة المرتبطة (JOUR، ID_CRENEAU):
إذا كان أحد صفوف الجدول [RV] يحتوي على القيمة (JOUR1، ID_CRENEAU1) في الأعمدة (JOUR، ID_CRENEAU)، فلا يمكن أن تظهر هذه القيمة في أي مكان آخر. وإلا، فهذا يعني أنه تم تسجيل قيمتين لـ RV في نفس الوقت لنفس الطبيب. من منظور برمجة Java، يقوم برنامج التشغيل JDBC الخاص بقاعدة البيانات بتشغيل برنامج SQLException عند حدوث هذه الحالة.
السطر الذي يحتوي على القيمة 3 لـ id (انظر [1] أعلاه) يعني أنه تم حجز موعد RV للفترة رقم 20 والعميل رقم 4 في 23/08/2006. يُظهر الجدول [CRENEAUX] أن الموعد رقم 20 يتوافق مع الفترة الزمنية من الساعة 16:20 إلى 16:40، وهو مخصص للطبيبة رقم 1 (السيدة ماري PELISSIER). توضح لنا الجدولة [CLIENTS] أن العميل رقم 4 هي الآنسة بريجيت BISTROU.
2.2. مقدمة إلى Spring Data
سنقوم بتنفيذ الطبقة [DAO] للمشروع باستخدام Spring Data، وهو فرع من نظام Spring.
![]() |
يوجد على موقع Spring العديد من الدروس التعليمية للبدء في استخدام Spring [http://spring.io/guides]. سنستخدم إحداها لتقديم Spring Data. ونستخدم في ذلك Spring Tool Suite (STS).
![]() |
- في [1]، نقوم باستيراد أحد البرامج التعليمية من [spring.io/guides]؛
![]() |
- في [2]، نختار البرنامج التعليمي [Accessing Data Jpa] الذي يوضح كيفية الوصول إلى قاعدة البيانات باستخدام Spring Data؛
- في [3]، نختار مشروعًا تم تكوينه بواسطة Maven؛
- في [4]، يمكن تقديم البرنامج التعليمي في شكلين: [initial] وهي نسخة فارغة يتم ملؤها باتباع البرنامج التعليمي، أو [complete] وهي النسخة النهائية من البرنامج التعليمي. نختار هذه الأخيرة؛
- في [5]، يمكننا اختيار عرض البرنامج التعليمي في متصفح؛
- في [6]، المشروع النهائي.
2.2.1. تكوين Maven للمشروع
يتم تكوين تبعيات Maven للمشروع في الملف [pom.xml]:
<groupId>org.springframework</groupId>
<artifactId>gs-accessing-data-jpa</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
<properties>
<!-- استخدم UTF-8 لكل شيء -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>hello.Application</start-class>
</properties>
- الأسطر 5-9: تحدد مشروع Maven أم. وهو الذي يحدد الجزء الأساسي من تبعيات المشروع. قد تكون هذه التبعيات كافية، وفي هذه الحالة لا نضيف المزيد، أو قد لا تكون كافية، وفي هذه الحالة نضيف التبعيات الناقصة؛
- الأسطر 12-15: تحدد تبعية لـ [spring-boot-starter-data-jpa]. يحتوي هذا الأرتيفاكت على فئات Spring Data؛
- الأسطر 16-19: تحدد تبعية لـ SGBD و H2 التي تسمح بإنشاء وإدارة قواعد البيانات في الذاكرة.
لنلقِ نظرة على الفئات التي توفرها هذه التبعيات:
![]() | ![]() | ![]() |
وهي عديدة جدًا:
- بعضها ينتمي إلى نظام Spring (تلك التي تبدأ بـ spring)؛
- وبعضها الآخر ينتمي إلى نظام Hibernate (hibernate، jboss) الذي نستخدم هنا تنفيذه JPA؛
- وبعضها الآخر عبارة عن مكتبات اختبار (junit، hamcrest)؛
- وأخرى عبارة عن مكتبات سجلات (log4j، logback، slf4j)؛
سنحتفظ بها جميعًا. أما بالنسبة لتطبيق قيد التشغيل، فيجب الاحتفاظ فقط بالمكتبات الضرورية.
في السطر 26 من الملف [pom.xml]، نجد السطر التالي:
<start-class>hello.Application</start-class>
يرتبط هذا السطر بالسطور التالية:
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
الأسطر 6-9، يتيح المكون الإضافي [spring-boot-maven-plugin] إنشاء ملف jar القابل للتنفيذ للتطبيق. ويشير السطر 26 من الملف [pom.xml] إلى الفئة القابلة للتنفيذ لهذا الملف jar.
2.2.2. الطبقة [JPA]
يتم الوصول إلى قاعدة البيانات من خلال طبقة [JPA]، Java Persistence API:
![]() |
![]() |
التطبيق بسيط ويتعامل مع العملاء [Customer]. الفئة [Customer] هي جزء من الطبقة [JPA] وهي كما يلي:
package hello;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String firstName;
private String lastName;
protected Customer() {
}
public Customer(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
}
}
يحتوي العميل على معرّف [id]، واسم أول [firstName]، واسم عائلة [lastName]. تمثل كل مثيل [Customer] سطرًا في جدول قاعدة البيانات.
- السطر 8: تعليق توضيحي JPA الذي يجعل من استدامة المثيلات [Customer] (إنشاء، قراءة، تحديث، حذف) تُدار بواسطة تنفيذ JPA. وبناءً على تبعيات Maven، نلاحظ أن التنفيذ JPA / Hibernate هو الذي يتم استخدامه؛
- السطران 11-12: تعليقات توضيحية لـ JPA تربط الحقل [id] بالمفتاح الأساسي لجدول [Customer]. يشير السطر 12 إلى أن التنفيذ JPA سيستخدم طريقة إنشاء المفتاح الأساسي الخاصة بـ SGBD المستخدم، وهو هنا H2؛
لا توجد تعليقات توضيحية أخرى لـ JPA. وبالتالي، سيتم استخدام القيم الافتراضية:
- ستحمل جدول [Customer] اسم الفئة، أي [Customer]؛
- وستحمل أعمدة هذا الجدول أسماء حقول الفئة: [id, firstName, lastName]، مع العلم أن الأحرف الكبيرة والصغيرة لا تؤخذ في الاعتبار في اسم عمود الجدول؛
تجدر الإشارة إلى أنه لم يتم ذكر اسم التنفيذ المستخدم JPA في أي مرحلة.
2.2.3. الطبقة [DAO]
![]() |
![]() |
تقوم الفئة [CustomerRepository] بتنفيذ الطبقة [DAO]. وفيما يلي شفرة البرمجة الخاصة بها:
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}
إنها إذن واجهة وليست فئة (السطر 7). وهي تمتد من واجهة [CrudRepository]، وهي واجهة تابعة لـ Spring Data (السطر 5). يتم تعريف هذه الواجهة بواسطة نوعين: الأول هو نوع العناصر المدارة، وهو هنا النوع [Customer]، والثاني هو نوع المفتاح الأساسي للعناصر المدارة، وهو هنا النوع [Long]. وتكون واجهة [CrudRepository] كما يلي:
package org.springframework.data.repository;
import java.io.Serializable;
@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> save(Iterable<S> entities);
T findOne(ID id);
boolean exists(ID id);
Iterable<T> findAll();
Iterable<T> findAll(Iterable<ID> ids);
long count();
void delete(ID id);
void delete(T entity);
void delete(Iterable<? extends T> entities);
void deleteAll();
}
تحدد هذه الواجهة العمليات CRUD (إنشاء – قراءة – تحديث – حذف) التي يمكن إجراؤها على النوع JPA T:
- السطر 8: تسمح طريقة save بحفظ كيان T في قاعدة البيانات. وهي تحفظ الكيان باستخدام المفتاح الأساسي الذي منحته إياه العملية SGBD. كما تسمح بتحديث كيان T المحدد بواسطة مفتاحه الأساسي id. ويتم اختيار إحدى العمليتين وفقًا لقيمة المفتاح الأساسي id: فإذا كانت قيمته null، تتم عملية الحفظ، وإلا تتم عملية التحديث؛
- السطر 10: نفس الأمر ولكن بالنسبة لقائمة من الكيانات؛
- السطر 12: تتيح الطريقة findOne استرجاع كيان T المحدد بواسطة مفتاحه الأساسي id؛
- السطر 22: تتيح الطريقة delete حذف كيان T المحدد بواسطة مفتاحه الأساسي id؛
- الأسطر 24-28: أشكال مختلفة من الطريقة [delete]؛
- السطر 16: تتيح الطريقة [findAll] استرجاع جميع الكيانات T المخزنة؛
- السطر 18: نفس الأمر، لكن يقتصر على الكيانات التي تم تمرير قائمة معرّفاتها؛
لنعد إلى الواجهة [CustomerRepository]:
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}
- السطر 9 يسمح بالبحث عن [Customer] باستخدام اسمه [lastName]؛
وهذا كل شيء بخصوص الطبقة [DAO]. لا توجد فئة لتنفيذ الواجهة السابقة. يتم إنشاء هذه الفئة عند التنفيذ بواسطة [Spring Data]. يتم تنفيذ أساليب الواجهة [CrudRepository] تلقائيًا. أما بالنسبة للأساليب المضافة إلى الواجهة [CustomerRepository]، فالأمر يختلف. لنعد إلى تعريف [Customer]:
private long id;
private String firstName;
private String lastName;
يتم تنفيذ الطريقة الموجودة في السطر 9 تلقائيًا بواسطة [Spring Data] لأنها تشير إلى الحقل [lastName] (السطر 3) في [Customer]. عندما يصادف Spring Data طريقة [findBySomething] في الواجهة المطلوب تنفيذها، فإنه ينفذها باستخدام الاستعلام التالي JPQL (لغة استعلامات Java Persistence):
لذلك يجب أن يحتوي النوع T على حقل باسم [something]. وبذلك فإن الطريقة
سيتم تنفيذها بواسطة كود مشابه لما يلي:
return [em].createQuery("select c from Customer c where c.lastName=:value").setParameter("value",lastName).getResultList()
حيث يشير [em] إلى سياق الاستمرارية JPA. ولا يكون ذلك ممكنًا إلا إذا كانت الفئة [Customer] تحتوي على حقل باسم [lastName]، وهو ما ينطبق هنا.
وختامًا، في الحالات البسيطة، تتيح لنا Spring Data تنفيذ الطبقة [DAO] باستخدام واجهة بسيطة.
2.2.4. الطبقة [console]
![]() |
![]() |
الفئة [Application] هي كما يلي:
package hello;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
// حفظ عدد قليل من العملاء
repository.save(new Customer("Jack", "Bauer"));
repository.save(new Customer("Chloe", "O'Brian"));
repository.save(new Customer("Kim", "Bauer"));
repository.save(new Customer("David", "Palmer"));
repository.save(new Customer("Michelle", "Dessler"));
// استدعاء جميع العملاء
Iterable<Customer> customers = repository.findAll();
System.out.println("Customers found with findAll():");
System.out.println("-------------------------------");
for (Customer customer : customers) {
System.out.println(customer);
}
System.out.println();
// استدعاء عميل واحد باستخدام ID
Customer customer = repository.findOne(1L);
System.out.println("Customer found with findOne(1L):");
System.out.println("--------------------------------");
System.out.println(customer);
System.out.println();
// استدعاء العملاء حسب الاسم الأخير
List<Customer> bauers = repository.findByLastName("Bauer");
System.out.println("Customer found with findByLastName('Bauer'):");
System.out.println("--------------------------------------------");
for (Customer bauer : bauers) {
System.out.println(bauer);
}
context.close();
}
}
- السطر 10: يشير إلى أن الفئة تُستخدم لتكوين Spring. يمكن بالفعل تكوين الإصدارات الحديثة من Spring بلغة Java بدلاً من XML. يمكن استخدام الطريقتين في آن واحد. في كود الفئة التي تحمل التعليق التوضيحي [Configuration]، نجد عادةً حبات Spring، أي تعريفات للفئات التي سيتم إنشاء مثيلات لها. هنا لم يتم تعريف أي حبة. تجدر الإشارة هنا إلى أنه عند العمل مع SGBD، يجب تعريف حبات Spring متنوعة:
- ملف [EntityManagerFactory] الذي يحدد التنفيذ JPA المطلوب استخدامه،
- ملف [DataSource] الذي يحدد مصدر البيانات المطلوب استخدامه،
- [TransactionManager] الذي يحدد مدير المعاملات المطلوب استخدامه؛
هنا لم يتم تعريف أي من هذه الفاصوليا.
- السطر 11: التعليق التوضيحي [EnableAutoConfiguration] هو تعليق توضيحي مستمد من المشروع [Spring Boot] (السطران 5-6). يطلب هذا التعليق التوضيحي من Spring Boot عبر الفئة [SpringApplication] (السطر 16) تكوين التطبيق وفقًا للمكتبات الموجودة في مسار الفئات (Classpath) الخاص به. ونظرًا لوجود مكتبات Hibernate في مسار الفئات، سيتم تنفيذ المكون [entityManagerFactory] باستخدام Hibernate. ونظرًا لوجود مكتبة SGBD H2 في مسار الفئات (Classpath)، فسيتم تنفيذ الكائن [dataSource] باستخدام H2. في الكائن [dataSource]، يجب أيضًا تعريف المستخدم وكلمة المرور الخاصة به. هنا سيستخدم Spring Boot المسؤول الافتراضي لـ H2، وهو بدون كلمة مرور. ونظرًا لوجود المكتبة [spring-tx] في مسار الفئات (Classpath)، فسيتم استخدام مدير المعاملات الخاص بـ Spring.
بالإضافة إلى ذلك، سيتم فحص المجلد الذي توجد فيه الفئة [Application] بحثًا عن «البيانات» (beans) التي يتعرف عليها Spring ضمناً أو التي تم تعريفها صراحةً بواسطة تعليقات Spring. وبالتالي، سيتم فحص الفئتين [Customer] و [CustomerRepository]. ونظرًا لأن الفئة الأولى تحمل التعليق التوضيحي [@Entity]، فسيتم تصنيفها ككيان يجب أن يديره Hibernate. ونظرًا لأن الفئة الثانية تمتد واجهة [CrudRepository]، فسيتم تسجيلها كـ bean في Spring.
لنلقِ نظرة على السطرين 16 و17 من الكود:
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
- السطر 1: يتم تنفيذ الطريقة الثابتة [run] الخاصة بالفئة [SpringApplication] في مشروع Spring Boot. معلمتها هي الفئة التي تحمل تعليقًا [Configuration] أو [EnableAutoConfiguration]. وعندئذٍ سيحدث كل ما تم شرحه سابقًا. والنتيجة هي سياق تطبيق Spring، أي مجموعة من الحبوب (beans) التي يديرها Spring؛
- السطر 17: نطلب من سياق Spring هذا حبة تُنفذ الواجهة [CustomerRepository]. ونحصل هنا على الفئة التي أنشأتها Spring Data لتنفيذ هذه الواجهة.
العمليات التالية لا تفعل سوى استخدام أساليب المكون الذي ينفذ الواجهة [CustomerRepository]. تجدر الإشارة في السطر 50 إلى أن السياق قد أُغلق. نتائج وحدة التحكم هي كما يلي:
- الأسطر 1-8: شعار مشروع Spring Boot؛
- السطر 9: يتم تنفيذ الفئة [hello.Application]؛
- السطر 10: [AnnotationConfigApplicationContext] هي فئة تُنفذ واجهة [ApplicationContext] الخاصة بـ Spring. وهي عبارة عن حاوية للفاصوليا (beans)؛
- السطر 11: يتم تنفيذ «البيان» [entityManagerFactory] باستخدام الفئة [LocalContainerEntityManagerFactory]، وهي فئة تابعة لـ Spring؛
- السطر 12: يظهر [hibernate]. وقد تم اختيار هذا التنفيذ JPA؛
- السطر 19: لهجة Hibernate هي المتغير SQL المراد استخدامه مع SGBD. هنا، تُظهر اللهجة [H2Dialect] أن Hibernate سيعمل مع SGBD وH2؛
- الأسطر 22-24: تم إنشاء الجدول [CUSTOMER]. وهذا يعني أن Hibernate قد تم تهيئته لإنشاء الجداول استنادًا إلى التعريفات JPA، وهنا التعريف JPA للفئة [Customer]؛
- الأسطر 27-32: سجلات Hibernate تُظهر عمليات إدراج الأسطر في الجدول [CUSTOMER]. وهذا يعني أن Hibernate قد تم تهيئته لإنشاء السجلات؛
- الأسطر 35-39: العملاء الخمسة الذين تم إدراجهم؛
- الأسطر 42-44: نتيجة الطريقة [findOne] الخاصة بالواجهة؛
- الأسطر 47-50: نتائج الطريقة [findByLastName]؛
- السطور 51 وما يليها: سجلات إغلاق سياق Spring.
2.2.5. التكوين اليدوي لمشروع Spring Data
نقوم بنسخ المشروع السابق إلى المشروع [gs-accessing-data-jpa-2]:
![]() |
في هذا المشروع الجديد، لن نعتمد على التكوين التلقائي الذي يقوم به Spring Boot. سنقوم به يدويًا. قد يكون ذلك مفيدًا إذا كانت الإعدادات الافتراضية لا تناسبنا.
أولاً، سنقوم بتحديد التبعيات الضرورية في الملف [pom.xml]:
<dependencies>
<!-- Spring Core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<!-- معاملات Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<!-- بيانات Spring -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.5.2.RELEASE</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>1.0.2.RELEASE</version>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.3.4.Final</version>
</dependency>
<!-- قاعدة بيانات H2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.178</version>
</dependency>
<!-- كومونز DBCP -->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
<version>1.6</version>
</dependency>
</dependencies>
- الأسطر 3-17: مكتبات Spring الأساسية؛
- الأسطر 19-28: مكتبات Spring لإدارة المعاملات مع قاعدة البيانات؛
- الأسطر 30-34: Spring Data المستخدم للوصول إلى قاعدة البيانات؛
- الأسطر 36-40: Spring Boot لتشغيل التطبيق؛
- الأسطر 48-52: SGBD H2؛
- الأسطر 54-63: غالبًا ما تُستخدم قواعد البيانات مع مجموعات من الاتصالات المفتوحة التي تتجنب فتح/إغلاق الاتصال بشكل متكرر. هنا، يتم استخدام تطبيق [commons-dbcp]؛
وفي [pom.xml] أيضًا، يتم تعديل اسم الفئة القابلة للتنفيذ:
<properties>
...
<start-class>demo.console.Main</start-class>
</properties>
في المشروع الجديد، لا تتغير الكيان [Customer] والواجهة [CustomerRepository]. سنقوم بتعديل الفئة [Application] التي سيتم تقسيمها إلى فئتين:
- [Config] التي ستكون فئة التكوين:
- [Main] التي ستكون الفئة القابلة للتنفيذ؛
![]() |
الفئة القابلة للتنفيذ [Main] هي نفسها كما في السابق بدون تعليقات التكوين:
package demo.console;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
public class Main {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Config.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
...
context.close();
}
}
- السطر 12: لم تعد الفئة [Main] تحتوي على تعليقات توضيحية للتكوين؛
- السطر 16: يتم تشغيل التطبيق باستخدام Spring Boot. المعلمة [Config.class] هي الفئة الجديدة لتكوين المشروع؛
الفئة [Config] التي تقوم بتكوين المشروع هي كما يلي:
package demo.config;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
//@ComponentScan(basePackages = { "demo" })
//@EntityScan(basePackages = { "demo.entities" })
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "demo.repositories" })
@Configuration
public class Config {
// مصدر البيانات H2
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:./demo");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
// المزود JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(true);
hibernateJpaVendorAdapter.setDatabase(Database.H2);
return hibernateJpaVendorAdapter;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan("demo.entities");
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// مدير المعاملات
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- السطر 22: تعمل العلامة التوضيحية [@Configuration] على تحويل الفئة [Config] إلى فئة تكوين Spring؛
- السطر 21: يسمح التعليق التوضيحي [@EnableJpaRepositories] بتحديد المجلدات التي توجد فيها واجهات Spring Data [CrudRepository]. ستصبح هذه الواجهات مكونات Spring وستكون متاحة في سياقها؛
- السطر 20: تشير العلامة التوضيحية [@EnableTransactionManagement] إلى أن أساليب واجهات [CrudRepository] يجب أن تُنفَّذ داخل معاملة؛
- السطر 19: تسمح العلامة التوضيحية [@EntityScan] بتحديد المجلدات التي يجب البحث فيها عن الكيانات JPA. وقد تم تعليقها هنا، لأن هذه المعلومة قد ذُكرت صراحةً في السطر 50. يجب أن يكون هذا التعليق موجودًا في حالة استخدام الوضع [@EnableAutoConfiguration]، وإذا لم تكن الكيانات JPA موجودة في نفس المجلد الذي توجد فيه فئة التكوين؛
- السطر 18: يسمح التعليق التوضيحي [@ComponentScan] بإدراج المجلدات التي يجب البحث فيها عن مكونات Spring. مكونات Spring هي فئات مُعلَّمة بعلامات Spring مثل @Service و@Component و@Controller، ... ولا توجد هنا أي مكونات أخرى غير تلك المُعرَّفة داخل الفئة [Config]، ولذلك تم تعليق العلامة؛
- الأسطر 25-33: تحدد مصدر البيانات، وهو قاعدة البيانات H2. إن التعليق التوضيحي @Bean في السطر 25 هو الذي يجعل الكائن الذي تم إنشاؤه بواسطة هذه الطريقة مكونًا يديره Spring. يمكن أن يكون اسم الأسلوب هنا أي اسم. ومع ذلك، يجب أن يُسمى [dataSource] إذا كان EntityManagerFactory في السطر 47 غير موجود وتم تعريفه عن طريق التكوين التلقائي؛
- السطر 29: ستسمى قاعدة البيانات [demo] وسيتم إنشاؤها في مجلد المشروع؛
- الأسطر 36-43: تحدد التنفيذ المستخدم JPA، وهو هنا تنفيذ Hibernate. يمكن أن يكون اسم الأسلوب هنا أي اسم؛
- السطر 39: لا توجد سجلات لـ SQL؛
- السطر 30: سيتم إنشاء قاعدة البيانات إذا لم تكن موجودة؛
- الأسطر 46-54: تحدد EntityManagerFactory التي ستدير استمرارية JPA. يجب أن يُسمى الأسلوب بالضرورة [entityManagerFactory]؛
- السطر 47: تتلقى الطريقة معلمتين من النوع الخاص بالفاصوليتين المحددتين سابقًا. سيتم بعد ذلك إنشاء هاتين الفاصوليتين ثم حقنهما بواسطة Spring كمعلمات للطريقة؛
- السطر 49: يحدد التنفيذ JPA المستخدم؛
- السطر 50: يحدد المجلدات التي توجد فيها الكيانات JPA؛
- السطر 51: يحدد مصدر البيانات المراد إدارته؛
- الأسطر 57-62: مدير المعاملات. يجب أن يُسمى الأسلوب بالضرورة [transactionManager]. ويتلقى كمعلمة «البيان» الموجود في الأسطر 46-54؛
- السطر 60: يتم ربط مدير المعاملات بـ EntityManagerFactory؛
يمكن تعريف الطرق السابقة بأي ترتيب.
يؤدي تنفيذ المشروع إلى الحصول على النتائج نفسها. يظهر ملف جديد في مجلد المشروع، وهو ملف قاعدة البيانات H2:
![]() |
وأخيرًا، يمكن الاستغناء عن Spring Boot. نقوم بإنشاء فئة قابلة للتنفيذ ثانيةً باسم [Main2]:
![]() |
تحتوي الفئة [Main2] على الكود التالي:
package demo.console;
import java.util.List;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
public class Main2 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
....
context.close();
}
}
- السطر 15: أصبحت فئة التكوين [Config] تُستخدم الآن بواسطة فئة Spring [AnnotationConfigApplicationContext]. ويمكن ملاحظة في السطر 5 أنه لم يعد هناك أي تبعية لـ Spring Boot.
يؤدي التنفيذ إلى نفس النتائج التي تم الحصول عليها سابقًا.
2.2.6. إنشاء أرشيف قابل للتنفيذ
لإنشاء ملف أرشيفي قابل للتنفيذ للمشروع، يمكن اتباع الخطوات التالية:
![]() |
- في [1]: يتم إنشاء تكوين للتنفيذ؛
- في [2]: من النوع [Java Application]
- في [3]: يحدد المشروع المراد تنفيذه (استخدم الزر Browse)؛
- في [4]: يشير إلى الفئة المراد تنفيذها؛
- في [5]: اسم تكوين التنفيذ – يمكن أن يكون أي اسم؛
![]() |
- في [6]: يتم تصدير المشروع؛
- في [7]: في شكل أرشيف قابل للتنفيذ JAR؛
- في [8]: يحدد المسار واسم الملف القابل للتنفيذ المراد إنشاؤه؛
- في [9]: اسم إعدادات التشغيل التي تم إنشاؤها في [5]؛
بعد ذلك، نفتح نافذة الأوامر في المجلد الذي يحتوي على الأرشيف القابل للتنفيذ:
يتم تشغيل الملف المضغوط بالطريقة التالية:
.....\dist>java -jar gs-accessing-data-jpa-2.jar
والنتائج التي تظهر في نافذة الأوامر هي كما يلي:
2.2.7. إنشاء مشروع Spring Data جديد
لإنشاء هيكل مشروع Spring Data، يمكن اتباع الخطوات التالية:
![]() |
- في [1]، نقوم بإنشاء مشروع جديد؛
- في [2]: من النوع [Spring Starter Project]؛
- سيكون المشروع الذي تم إنشاؤه مشروعًا من نوع Maven. في [3]، نحدد اسم مجموعة المشروع؛
- في [4]: نحدد اسم الأرتيفاكت (ملف jar هنا) الذي سيتم إنشاؤه عند بناء المشروع؛
- في [5]: يُحدد حزمة الفئة القابلة للتنفيذ التي سيتم إنشاؤها في المشروع؛
- في [6]: اسم المشروع في Eclipse – يمكن أن يكون أي اسم (لا يجب أن يكون مطابقًا لـ [4])؛
- في [7]: يُشار إلى أنه سيتم إنشاء مشروع يحتوي على طبقة [JPA]. وسيتم عندئذٍ تضمين التبعيات اللازمة لمثل هذا المشروع في الملف [pom.xml]؛
![]() |
- في [8]: المشروع الذي تم إنشاؤه؛
يحتوي الملف [pom.xml] على التبعيات اللازمة لمشروع JPA:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.0.RELEASE</version>
<relativePath/> <!-- البحث عن العنصر الأصلي من المستودع -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- الأسطر 9-12: التبعيات اللازمة لـ JPA – ستشمل [Spring Data]؛
- الأسطر 13-17: التبعيات اللازمة لاختبارات JUnit المدمجة مع Spring؛
الفئة القابلة للتنفيذ [Application] لا تقوم بأي شيء ولكنها مُعدة مسبقًا:
package istia.st;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
فئة الاختبارات [ApplicationTests] لا تقوم بأي شيء ولكنها مُهيأة مسبقًا:
package istia.st;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
@Test
public void contextLoads() {
}
}
- السطر 9: يسمح التعليق التوضيحي [@SpringApplicationConfiguration] باستخدام ملف التكوين [Application]. وبذلك ستستفيد فئة الاختبار من جميع الفاصوليا التي سيتم تعريفها بواسطة هذا الملف؛
- السطر 8: تسمح العلامة [@RunWith] بدمج Spring مع JUnit: سيتمكن تنفيذ الفئة كاختبار JUnit. [@RunWith] هو تعليق توضيحي لـ JUnit (السطر 4) في حين أن الفئة [SpringJUnit4ClassRunner] هي فئة Spring (السطر 6)؛
والآن بعد أن أصبح لدينا هيكل أساسي للتطبيق JPA، يمكننا استكماله لكتابة مشروع طبقة الاستمرارية للخادم في تطبيقنا لإدارة المواعيد.
2.3. مشروع Eclipse الخاص بالخادم
![]() |
![]() |
العناصر الرئيسية للمشروع هي كما يلي:
- [pom.xml]: ملف تكوين Maven للمشروع؛
- [rdvmedecins.entities]: كيانات JPA؛
- [rdvmedecins.repositories]: واجهات Spring Data للوصول إلى الكيانات JPA؛
- [rdvmedecins.metier]: الطبقة [métier]؛
- [rdvmedecins.domain]: الكيانات التي تعالجها الطبقة [métier]؛
- [rdvmdecins.config]: فئات التكوين الخاصة بطبقة الاستمرارية؛
- [rdvmedecins.boot]: تطبيق وحدة تحكم بسيط؛
2.4. تكوين Maven
![]() | ![]() | ![]() |
ملف [pom.xml] الخاص بالمشروع هو كما يلي:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
</dependencies>
<properties>
<!-- استخدم UTF-8 لكل شيء -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>istia.st.spring.data.main.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>org.jboss.repository.releases</id>
<name>JBoss Maven Release Repository</name>
<url>https://repository.jboss.org/nexus/content/repositories/releases</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
- الأسطر 8-12: يعتمد المشروع على المشروع الأصلي [spring-boot-starter-parent]. بالنسبة للتبعيات الموجودة بالفعل في المشروع الأصلي، لا يتم تحديد إصدار. سيتم استخدام الإصدار المحدد في المشروع الأصلي. أما بالنسبة للتبعيات الأخرى، فيتم الإعلان عنها بشكل عادي؛
- الأسطر 14-17: بالنسبة لـ Spring Data؛
- الأسطر 18-22: للاختبارات JUnit؛
- الأسطر 23-26: برنامج تجريبي JDBC لـ SGBD و MySQL5؛
- الأسطر 27-34: تجمع اتصالات Commons DBCP؛
- الأسطر 35-38: مكتبة Jackson لإدارة JSON؛
- الأسطر 39-43: مكتبة Google لإدارة المجموعات؛
تستخدم النسخة 1.1.0.RC1 من [spring-boot-starter-parent] الإصدارات التالية من المكتبات:
2.5. الكيانات JPA
![]() |
الكيانات JPA هي الكائنات التي ستقوم بتغليف صفوف جداول قاعدة البيانات.
![]() |
الفئة [AbstractEntity] هي الفئة الأم للكيانات [Personne, Creneau, Rv]. وتعريفها كما يلي:
package rdvmedecins.entities;
import java.io.Serializable;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
@MappedSuperclass
public class AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
protected Long id;
@Version
protected Long version;
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
// التهيئة
public AbstractEntity build(Long id, Long version) {
this.id = id;
this.version = version;
return this;
}
@Override
public boolean equals(Object entity) {
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return this.id == other.id;
}
// الوصول إلى القيم وتعيينها
..
}
- السطر 11: يشير التعليق التوضيحي [@MappedSuperclass] إلى أن الفئة المُعلَّقة عليها هي الفئة الأم للكيانات JPA و[@Entity]؛
- الأسطر 15-17: تحدد المفتاح الأساسي [id] لكل كيان. التعليق التوضيحي [@Id] هو الذي يجعل الحقل [id] مفتاحًا أساسيًا. تشير التعليقات التوضيحية [@GeneratedValue(strategy = GenerationType.AUTO)] إلى أن قيمة هذا المفتاح الأساسي يتم إنشاؤها بواسطة SGBD وأنه لا يوجد نمط إنشاء مفروض؛
- السطران 18-19: يحددان إصدار كل كيان. سيقوم التنفيذ JPA بزيادة رقم الإصدار هذا في كل مرة يتم فيها تعديل الكيان. يُستخدم رقم الإصدار هذا لمنع التحديث المتزامن للكيان من قِبل مستخدمين مختلفين: يقوم مستخدمان هما U1 وU2 بقراءة الكيان E الذي يحمل رقم إصدار يساوي V1. يقوم المستخدم U1 بتعديل الكيان E وحفظ هذا التعديل في قاعدة البيانات: فيتغير رقم الإصدار عندئذٍ إلى V1+1. U2 يقوم بدوره بتعديل E ويحفظ هذا التعديل في قاعدة البيانات: سيتلقى استثناءً لأنه يمتلك إصدارًا (V1) يختلف عن الإصدار الموجود في قاعدة البيانات (V1+1)؛
- الأسطر 29-33: تسمح الطريقة [build] بتهيئة الحقلين في [AbstractEntity]. وتؤدي هذه الطريقة إلى تهيئة مرجع المثيل [AbstractEntity]؛
- الأسطر 36-44: يتم إعادة تعريف الطريقة [equals] الخاصة بالفئة: يُعتبر كيانان متساويين إذا كان لهما نفس اسم الفئة ونفس المعرف id؛
الكيان [Personne] هو الفئة الأم للكيانين [Medecin] و [Client]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
@MappedSuperclass
public class Personne extends AbstractEntity {
private static final long serialVersionUID = 1L;
// سمات الشخص
@Column(length = 5)
private String titre;
@Column(length = 20)
private String nom;
@Column(length = 20)
private String prenom;
// منشئ افتراضي
public Personne() {
}
// منشئ مع معلمات
public Personne(String titre, String nom, String prenom) {
this.titre = titre;
this.nom = nom;
this.prenom = prenom;
}
// toString
public String toString() {
return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
}
// دالات الاسترجاع والتعيين
...
}
- السطر 6: تشير التعليقات التوضيحية [@MappedSuperclass] إلى أن الفئة المُعلَّقة عليها هي الفئة الأم للكيانات JPA و[@Entity]؛
- الأسطر 10-15: شخص ما له لقب (Melle)، واسم أول (Jacqueline)، واسم عائلة (Tatou). لم تُقدم أي معلومات عن أعمدة الجدول. لذا ستحمل هذه الأعمدة افتراضيًا نفس أسماء الحقول؛
الكيان [Medecin] هو كما يلي:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "medecins")
public class Medecin extends Personne {
private static final long serialVersionUID = 1L;
// منشئ افتراضي
public Medecin() {
}
// منشئ مع معلمات
public Medecin(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
public String toString() {
return String.format("Medecin[%s]", super.toString());
}
}
- السطر 6: الفئة هي كيان JPA؛
- السطر 7: مرتبطة بالجدول [MEDECINS] في قاعدة البيانات؛
- السطر 8: الكيان [Medecin] مشتق من الكيان [Personne]؛
يمكن تهيئة طبيب بالطريقة التالية:
وإذا أردنا أيضًا تعيين معرّف وإصدار له، فيمكننا كتابة:
حيث إن الطريقة [build] هي تلك المُعرَّفة في [AbstractEntity].
الكيان [Client] هو كما يلي:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "clients")
public class Client extends Personne {
private static final long serialVersionUID = 1L;
// منشئ افتراضي
public Client() {
}
// منشئ مع معلمات
public Client(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
// الهوية
public String toString() {
return String.format("Client[%s]", super.toString());
}
}
- السطر 6: الفئة هي كيان JPA؛
- السطر 7: مرتبطة بالجدول [CLIENTS] في قاعدة البيانات؛
- السطر 8: الكيان [Client] مشتق من الكيان [Personne]؛
الكيان [Creneau] هو كما يلي:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// خصائص موعد من نوع RV
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// الفتحة مرتبطة بطبيب
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// مفتاح خارجي
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
// المُنشئ الافتراضي
public Creneau() {
}
// مُنشئ مع معلمات
public Creneau(Medecin medecin, int hdebut, int mdebut, int hfin, int mfin) {
this.medecin = medecin;
this.hdebut = hdebut;
this.mdebut = mdebut;
this.hfin = hfin;
this.mfin = mfin;
}
// toString
public String toString() {
return String.format("Créneau[%d, %d, %d, %d:%d, %d:%d]", id, version, idMedecin, hdebut, mdebut, hfin, mfin);
}
// مفتاح خارجي
public long getIdMedecin() {
return idMedecin;
}
// مُعيِّنات - مُستخرِجات
...
}
- السطر 10: الفئة هي الكيان JPA؛
- السطر 11: مرتبطة بالجدول [CRENEAUX] في قاعدة البيانات؛
- السطر 12: الكيان [Creneau] مشتق من الكيان [AbstractEntity]، وبالتالي يرث المعرف [id] والإصدار [version]؛
- السطر 16: وقت بدء الفترة الزمنية (14)؛
- السطر 17: دقائق بداية الفترة الزمنية (20)؛
- السطر 18: ساعة انتهاء الفترة الزمنية (14)؛
- السطر 19: دقائق انتهاء الفترة الزمنية (40)؛
- الأسطر 22-24: الطبيب المالك للفترة. تحتوي الجدول [CRENEAUX] على مفتاح خارجي مرتبط بالجدول [MEDECINS]. تتجسد هذه العلاقة في الأسطر 22-24؛
- السطر 22: تشير العلامة [@ManyToOne] إلى علاقة «عدة (فترات) إلى واحد (طبيب)». يشير السمة [fetch=FetchType.LAZY] إلى أنه عند طلب كيان [Creneau] من سياق الاستمرارية، وإذا كان لا بد من البحث عنه في قاعدة البيانات، فإن الكيان [Medecin] لا يتم إرجاعه معه. وتكمن فائدة هذا الوضع في أن الكيان [Medecin] لا يتم البحث عنه إلا إذا طلب المطور ذلك. وبذلك يتم توفير الذاكرة وتحسين الأداء؛
- السطر 23: يشير إلى اسم عمود المفتاح الأجنبي في الجدول [CRENEAUX]؛
- السطران 27-28: المفتاح الأجنبي في الجدول [MEDECINS]؛
- السطر 27: تم استخدام العمود [ID_MEDECIN] بالفعل في السطر 23. وهذا يعني أنه يمكن تعديله بطريقتين مختلفتين، وهو ما لا يقبله المعيار JPA. لذلك نضيف السمات [insertable = false, updatable = false]، مما يجعل العمود قابلاً للقراءة فقط؛
الكيان [Rv] هو كما يلي:
package rdvmedecins.entities;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// خصائص Rv
@Temporal(TemporalType.DATE)
private Date jour;
// يرتبط rv بعميل
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// Rv مرتبط بفترة زمنية
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// المفاتيح الخارجية
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
// الشركة المصنعة الافتراضية
public Rv() {
}
// مع المعلمات
public Rv(Date jour, Client client, Creneau creneau) {
this.jour = jour;
this.client = client;
this.creneau = creneau;
}
// toString
public String toString() {
return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
}
// مفاتيح خارجية
public long getIdCreneau() {
return idCreneau;
}
public long getIdClient() {
return idClient;
}
// مُستردات وقيم
...
}
- السطر 14: الفئة هي كيان JPA؛
- السطر 15: مرتبطة بالجدول [RV] في قاعدة البيانات؛
- السطر 16: الكيان [Rv] مشتق من الكيان [AbstractEntity]، وبالتالي يرث المعرف [id] والإصدار [version]؛
- السطر 21: تاريخ الموعد؛
- السطر 20: يحتوي النوع [Date] في Java على تاريخ ووقت معًا. هنا نحدد أنه يتم استخدام التاريخ فقط؛
- الأسطر 24-26: العميل الذي تم تحديد هذا الموعد من أجله. تحتوي الجدولة [RV] على مفتاح خارجي مرتبط بالجدولة [CLIENTS]. وتظهر هذه العلاقة في الأسطر 24-26؛
- الأسطر 29-31: الفترة الزمنية للموعد. تحتوي الجدولة [RV] على مفتاح خارجي مرتبط بالجدولة [CRENEAUX]. وتجسد الأسطر 29-31 هذه العلاقة؛
- السطور 34-35: المفتاح الأجنبي [idClient]؛
- الصفوف 36-37: المفتاح الأجنبي [idCreneau]؛
2.6. الطبقة [DAO]
![]() |
سنقوم بتنفيذ الطبقة [DAO] باستخدام Spring Data:
![]() |
يتم تنفيذ الطبقة [DAO] باستخدام أربع واجهات Spring Data:
- [ClientRepository]: تتيح الوصول إلى الكيانات JPA و[Client]؛
- [CreneauRepository]: يتيح الوصول إلى الكيانات JPA و [Creneau]؛
- [MedecinRepository]: يتيح الوصول إلى الكيانات JPA و [Medecin]؛
- [RvRepository]: يتيح الوصول إلى الكيانات JPA و [Rv]؛
واجهة [MedecinRepository] هي كما يلي:
package rdvmedecins.repositories;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Medecin;
public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
- السطر 7: تكتفي الواجهة [MedecinRepository] برث الطرق من الواجهة [CrudRepository] دون إضافة طرق أخرى؛
الواجهة [ClientRepository] هي كما يلي:
package rdvmedecins.repositories;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Client;
public interface ClientRepository extends CrudRepository<Client, Long> {
}
- السطر 7: تكتفي الواجهة [ClientRepository] بوراثة الطرق من الواجهة [CrudRepository] دون إضافة طرق أخرى؛
الواجهة [CreneauRepository] هي كما يلي:
package rdvmedecins.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Creneau;
public interface CreneauRepository extends CrudRepository<Creneau, Long> {
// قائمة المواعيد المتاحة للطبيب
@Query("select c from Creneau c where c.medecin.id=?1")
Iterable<Creneau> getAllCreneaux(long idMedecin);
}
- السطر 8: ترث الواجهة [CreneauRepository] الطرق الموجودة في الواجهة [CrudRepository]؛
- السطران 10-11: تتيح الطريقة [getAllCreneaux] الحصول على المواعيد المتاحة لطبيب ما؛
- السطر 11: المعلمة هي معرّف الطبيب. والنتيجة هي قائمة بالمواعيد المتاحة في شكل كائن [Iterable<Creneau>]؛
- السطر 10: يسمح التعليق التوضيحي [@Query] بتحديد الاستعلام JPQL (لغة استعلامات الاستمرارية في Java) الذي ينفذ الطريقة. سيتم استبدال المعلمة [?1] بالمعلمة [idMedecin] الخاصة بالطريقة؛
الواجهة [RvRepository] هي كما يلي:
package rdvmedecins.repositories;
import java.util.Date;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Rv;
public interface RvRepository extends CrudRepository<Rv, Long> {
@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
Iterable<Rv> getRvMedecinJour(long idMedecin, Date jour);
}
- السطر 10: ترث الواجهة [RvRepository] الطرق من الواجهة [CrudRepository]؛
- السطران 12-13: تتيح الطريقة [getRvMedecinJour] الحصول على مواعيد الطبيب ليوم معين؛
- السطر 13: المعلمات هي معرّف الطبيب واليوم. والنتيجة هي قائمة بالمواعيد في شكل كائن [Iterable<Rv>]؛
- السطر 12: يسمح التعليق التوضيحي [@Query] بتحديد الاستعلام JPQL الذي ينفذ الطريقة. سيتم استبدال المعلمة [?1] بالمعلمة [idMedecin] الخاصة بالطريقة، وسيتم استبدال المعلمة [?2] بالمعلمة [jour] الخاصة بالطريقة. لا يمكن الاكتفاء بالاستعلام JPQL التالي:
لأن حقول الفئة Rv، من الأنواع [Client] و [Creneau]، يتم الحصول عليها في الوضع [FetchType.LAZY]، مما يعني أنه يجب طلبها صراحةً للحصول عليها. ويتم ذلك في الاستعلام JPQL باستخدام صيغة [left join fetch entité] التي تطلب إجراء ربط مع الجدول الذي يشير إليه المفتاح الأجنبي من أجل استرداد الكيان المشار إليه؛
2.7. الطبقة [métier]
![]() |
![]() |
- [IMetier] هي واجهة الطبقة [métier]، و [Metier] هي تنفيذها؛
- [AgendaMedecinJour] و [CreneauMedecinJour] هما كيانان من كيانات الأعمال؛
2.7.1. الكيانات
تربط الكيان [CreneauMedecinJour] بين فترة زمنية وموعد محتمل تم تحديده خلال تلك الفترة:
package rdvmedecins.domain;
import java.io.Serializable;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// الحقول
private Creneau creneau;
private Rv rv;
// المنشئات
public CreneauMedecinJour() {
}
public CreneauMedecinJour(Creneau creneau, Rv rv) {
this.creneau=creneau;
this.rv=rv;
}
// toString
@Override
public String toString() {
return String.format("[%s %s]", creneau, rv);
}
// أدوات الاسترجاع والتعيين
...
}
- السطر 12: الفترة الزمنية؛
- السطر 13: الموعد المحتمل – null بخلاف ذلك؛
الكيان [AgendaMedecinJour] هو جدول مواعيد الطبيب ليوم معين، أي قائمة مواعيده:
package rdvmedecins.domain;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import rdvmedecins.entities.Medecin;
public class AgendaMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// الحقول
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
// منشئات
public AgendaMedecinJour() {
}
public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
this.medecin = medecin;
this.jour = jour;
this.creneauxMedecinJour = creneauxMedecinJour;
}
public String toString() {
StringBuffer str = new StringBuffer("");
for (CreneauMedecinJour cr : creneauxMedecinJour) {
str.append(" ");
str.append(cr.toString());
}
return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
}
// دالات الاسترجاع والتعيين
...
}
- السطر 13: الطبيب؛
- السطر 14: اليوم في جدول المواعيد؛
- السطر 15: فتراته الزمنية سواء كانت محجوزة بموعد أم لا؛
2.7.2. الخدمة
واجهة الطبقة [métier] هي كما يلي:
package rdvmedecins.metier;
import java.util.Date;
import java.util.List;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
public interface IMetier {
// قائمة العملاء
public List<Client> getAllClients();
// قائمة الأطباء
public List<Medecin> getAllMedecins();
// قائمة المواعيد المتاحة للطبيب
public List<Creneau> getAllCreneaux(long idMedecin);
// قائمة مواعيد الطبيب في يوم معين
public List<Rv> getRvMedecinJour(long idMedecin, Date jour);
// البحث عن عميل باستخدام رقم التعريف الخاص به
public Client getClientById(long id);
// البحث عن عميل باستخدام رقم التعريف الخاص به
public Medecin getMedecinById(long id);
// البحث عن موعد محدد برقم التعريف الخاص به
public Rv getRvById(long id);
// البحث عن فترة زمنية محددة برقم التعريف الخاص بها
public Creneau getCreneauById(long id);
// إضافة RV
public Rv ajouterRv(Date jour, Creneau créneau, Client client);
// حذف RV
public void supprimerRv(Rv rv);
// المهنة
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);
}
تشرح التعليقات دور كل طريقة.
تنفيذ واجهة [IMetier] هو الفئة [Metier] التالية:
package rdvmedecins.metier;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.domain.CreneauMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.repositories.ClientRepository;
import rdvmedecins.repositories.CreneauRepository;
import rdvmedecins.repositories.MedecinRepository;
import rdvmedecins.repositories.RvRepository;
import com.google.common.collect.Lists;
@Service("métier")
public class Metier implements IMetier {
// مستودعات
@Autowired
private MedecinRepository medecinRepository;
@Autowired
private ClientRepository clientRepository;
@Autowired
private CreneauRepository creneauRepository;
@Autowired
private RvRepository rvRepository;
// تنفيذ الواجهة
@Override
public List<Client> getAllClients() {
return Lists.newArrayList(clientRepository.findAll());
}
@Override
public List<Medecin> getAllMedecins() {
return Lists.newArrayList(medecinRepository.findAll());
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return Lists.newArrayList(creneauRepository.getAllCreneaux(idMedecin));
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return Lists.newArrayList(rvRepository.getRvMedecinJour(idMedecin, jour));
}
@Override
public Client getClientById(long id) {
return clientRepository.findOne(id);
}
@Override
public Medecin getMedecinById(long id) {
return medecinRepository.findOne(id);
}
@Override
public Rv getRvById(long id) {
return rvRepository.findOne(id);
}
@Override
public Creneau getCreneauById(long id) {
return creneauRepository.findOne(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau créneau, Client client) {
return rvRepository.save(new Rv(jour, client, créneau));
}
@Override
public void supprimerRv(Rv rv) {
rvRepository.delete(rv.getId());
}
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
...
}
}
- السطر 24: التعليق التوضيحي [@Service] هو تعليق توضيحي لـ Spring يجعل الفئة المُعلَّمة مكونًا يديره Spring. يمكن تسمية المكون أو عدم تسميته. وقد سُمي هذا المكون [métier]؛
- السطر 25: الفئة [Metier] تُنفِّذ الواجهة [IMetier]؛
- السطر 28: التعليق التوضيحي [@Autowired] هو تعليق توضيحي من Spring. سيتم تهيئة (حقن) قيمة الحقل المُعلَّم بهذه الطريقة بواسطة Spring باستخدام مرجع مكون Spring من النوع أو الاسم المحدد. هنا، لا تحدد العلامة [@Autowired] أي اسم. وبالتالي، سيتم إجراء حقن حسب النوع؛
- السطر 29: سيتم تهيئة الحقل [medecinRepository] بإشارة إلى مكون Spring من النوع [MedecinRepository]. وستكون هذه الإشارة إلى الفئة التي تم إنشاؤها بواسطة Spring Data لتنفيذ الواجهة [MedecinRepository] التي سبق أن عرضناها؛
- الأسطر 30-35: تتكرر هذه العملية بالنسبة للواجهات الثلاث الأخرى التي تمت دراستها؛
- الأسطر 39-41: تنفيذ الطريقة [getAllClients]؛
- السطر 40: نستخدم الطريقة [findAll] من الواجهة [ClientRepository]. تُرجع هذه الطريقة نوعًا [Iterable<Client>] الذي نقوم بتحويله إلى [List<Client>] باستخدام الطريقة الثابتة [Lists.newArrayList]. تم تعريف الفئة [Lists] في مكتبة Google Guava. وفي [pom.xml]، تم استيراد هذا التبعية:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
- الأسطر 38-86: يتم تنفيذ طرق واجهة [IMetier] بمساعدة فئات الطبقة [DAO]؛
الطريقة الموجودة في السطر 88 هي الوحيدة الخاصة بالطبقة [métier]. وقد وُضعت هنا لأنها تقوم بمعالجة متخصصة لا تقتصر على مجرد الوصول إلى البيانات. لولا هذه الطريقة، لما كان هناك داعٍ لإنشاء طبقة [métier]. الطريقة [getAgendaMedecinJour] هي كما يلي:
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
// قائمة المواعيد المتاحة للطبيب
List<Creneau> creneauxHoraires = getAllCreneaux(idMedecin);
// قائمة حجوزات هذا الطبيب نفسه لهذا اليوم نفسه
List<Rv> reservations = getRvMedecinJour(idMedecin, jour);
// يتم إنشاء قاموس بناءً على المواعيد المحجوزة
Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
for (Rv resa : reservations) {
hReservations.put(resa.getCreneau().getId(), resa);
}
// يتم إنشاء جدول المواعيد لليوم المطلوب
AgendaMedecinJour agenda = new AgendaMedecinJour();
// الطبيب
agenda.setMedecin(getMedecinById(idMedecin));
// اليوم
agenda.setJour(jour);
// فترات الحجز
CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
agenda.setCreneauxMedecinJour(creneauxMedecinJour);
// ملء فترات الحجز
for (int i = 0; i < creneauxHoraires.size(); i++) {
// السطر i في جدول المواعيد
creneauxMedecinJour[i] = new CreneauMedecinJour();
// الفترة الزمنية
Creneau créneau = creneauxHoraires.get(i);
long idCreneau = créneau.getId();
creneauxMedecinJour[i].setCreneau(créneau);
// هل الفترة الزمنية متاحة أم محجوزة؟
if (hReservations.containsKey(idCreneau)) {
// الفترة مشغولة - يتم تسجيل الحجز
Rv resa = hReservations.get(idCreneau);
creneauxMedecinJour[i].setRv(resa);
}
}
// يتم إرجاع النتيجة
return agenda;
}
يُرجى من القارئ الاطلاع على التعليقات. والخوارزمية هي كما يلي:
- يتم استرداد جميع المواعيد المتاحة للطبيب المحدد؛
- نسترد جميع مواعيده في اليوم المحدد؛
- باستخدام هاتين المعلومتين، يمكننا تحديد ما إذا كان الموعد متاحًا أم مشغولًا؛
2.8. تكوين المشروع
![]() |
تقوم الفئة [DomainAndPersitenceConfig] بتكوين المشروع بأكمله:
package rdvmedecins.config;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.orm.jpa.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
// مصدر البيانات MySQL
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
dataSource.setUsername("root");
dataSource.setPassword("");
return dataSource;
}
// المزود JPA - غير ضروري إذا كنا راضين عن القيم الافتراضية التي يستخدمها Spring Boot
// هنا يتم تعريفه لتفعيل/إيقاف تشغيل السجلات SQL
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
return hibernateJpaVendorAdapter;
}
// يتم تعريف كل من EntityManagerFactory و TransactionManager بقيم افتراضية بواسطة Spring Boot
}
- السطر 45: لن نقوم بتعريف الفئات [EntityManagerFactory] و [TransactionManager]. سنعتمد في ذلك على تعليق Spring Boot [@EnableAutoConfiguration] (السطر 17)؛
- الأسطر 24-32: تحدد مصدر البيانات MySQL5. وهو bean لا يمكن لـ Spring Boot عادةً توقعه؛
- الأسطر 36-43: نقوم أيضًا بتكوين التنفيذ JPA لتعيين السمة [showSql] في Hibernate على «false» (السطر 39). بشكل افتراضي، تكون هذه السمة على «true»؛
- في الوقت الحالي، المكونات الوحيدة التي يديرها Spring هي البينات الموجودة في الأسطر 25 و37 بالإضافة إلى البينات [EntityManagerFactory] و[TransactionManager] عن طريق التكوين التلقائي. علينا إضافة حبات الطبقات [métier] و [DAO]؛
- يضيف السطر 16 إلى سياق Spring واجهات الحزمة [rdvmdecins.repositories] التي ترث الواجهة [CrudRepository]؛
- تضيف السطر 18 إلى سياق Spring جميع فئات الحزمة [rdvmedecins] وفئاتها المشتقة التي تحتوي على تعليق Spring. في الحزمة [rdvmdecins.metier]، سيتم العثور على الفئة [Metier] مع تعليقها [@Service] وإضافتها إلى سياق Spring؛
- السطر 45: سيتم تعريف bean [entityManagerFactory] افتراضيًا بواسطة Spring Boot. يجب إرشاد هذا bean إلى مواقع الكيانات JPA التي يجب عليه إدارتها. والسطر 19 هو الذي يقوم بذلك؛
- السطر 20: يشير إلى أن أساليب الواجهات التي ترث الواجهة [CrudRepository] يجب أن تُنفَّذ ضمن معاملة؛
2.9. اختبارات الطبقة [métier]
الفئة [rdvmedecins.tests.Metier] هي فئة اختبار Spring / JUnit 4:
package rdvmedecins.tests;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Metier {
@Autowired
private IMetier métier;
@Test
public void test1(){
// عرض العملاء
List<Client> clients = métier.getAllClients();
display("Liste des clients :", clients);
// عرض الأطباء
List<Medecin> medecins = métier.getAllMedecins();
display("Liste des médecins :", medecins);
// عرض المواعيد المتاحة لأحد الأطباء
Medecin médecin = medecins.get(0);
List<Creneau> creneaux = métier.getAllCreneaux(médecin.getId());
display(String.format("Liste des créneaux du médecin %s", médecin), creneaux);
// قائمة مواعيد الطبيب في يوم معين
Date jour = new Date();
display(String.format("Liste des rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// إضافة RV
Rv rv = null;
Creneau créneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
rv = métier.ajouterRv(jour, créneau, client);
// التحقق
Rv rv2 = métier.getRvById(rv.getId());
Assert.assertEquals(rv, rv2);
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// إضافة RV في نفس الموعد من نفس اليوم
// يجب أن يؤدي إلى حدوث استثناء
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
Boolean erreur = false;
try {
rv = métier.ajouterRv(jour, créneau, client);
System.out.println("Rv ajouté");
} catch (Exception ex) {
Throwable th = ex;
while (th != null) {
System.out.println(ex.getMessage());
th = th.getCause();
}
// يتم تسجيل الخطأ
erreur = true;
}
// نتحقق من وجود خطأ
Assert.assertTrue(erreur);
// قائمة بـ RV
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// عرض التقويم
AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
System.out.println(agenda);
Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
// حذف RV
System.out.println("Suppression du Rv ajouté");
métier.supprimerRv(rv);
// التحقق
rv2 = métier.getRvById(rv.getId());
Assert.assertNull(rv2);
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
}
// طريقة الأداة المساعدة - تعرض عناصر المجموعة
private void display(String message, Iterable<?> elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- السطر 22: تسمح العلامة التوضيحية [@SpringApplicationConfiguration] باستخدام ملف التكوين [DomainAndPersistenceConfig] الذي تمت دراسته سابقًا. وبذلك تستفيد فئة الاختبار من جميع الفاصوليا المحددة في هذا الملف؛
- السطر 23: تسمح التعليقات التوضيحية [@RunWith] بدمج Spring مع JUnit: ستتمكن الفئة من التنفيذ كاختبار JUnit. [@RunWith] هو تعليق توضيحي JUnit (السطر 9) في حين أن الفئة [SpringJUnit4ClassRunner] هي فئة Spring (السطر 12)؛
- السطران 26-27: حقن مرجع إلى الطبقة [métier] في فئة الاختبار؛
- العديد من الاختبارات ليست سوى اختبارات بصرية بسيطة:
- السطران 32-33: قائمة العملاء؛
- السطران 35-36: قائمة الأطباء؛
- السطران 39-40: قائمة المواعيد المتاحة لأحد الأطباء؛
- السطر 43: قائمة مواعيد الطبيب؛
- السطر 50: إضافة موعد جديد. تعرض الطريقة [ajouterRv] الموعد مع معلومة إضافية، وهي مفتاحه الأساسي id؛
- السطر 53: يتم استخدام هذا المفتاح الأساسي للبحث عن الموعد في قاعدة البيانات؛
- السطر 54: يتم التحقق من أن الموعد المطلوب والموعد الذي تم العثور عليه هما نفس الموعد. تجدر الإشارة إلى أن الطريقة [equals] الخاصة بالكيان [Rv] قد أُعيد تعريفها: يعتبر الموعدان متطابقين إذا كان لهما نفس المفتاح الأساسي (id). وهذا يوضح لنا هنا أن الموعد المضاف قد أُدرج بالفعل في قاعدة البيانات؛
- الأسطر 61-73: نحاول إضافة الموعد نفسه للمرة الثانية. يجب أن يرفض ذلك الكيان SGBD نظرًا لوجود قيد التفرد:
CREATE TABLE IF NOT EXISTS `rv` (
`ID` bigint(20) NOT NULL AUTO_INCREMENT,
`JOUR` date NOT NULL,
`ID_CLIENT` bigint(20) NOT NULL,
`ID_CRENEAU` bigint(20) NOT NULL,
`VERSION` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`ID`),
UNIQUE KEY `UNQ1_RV` (`JOUR`,`ID_CRENEAU`),
KEY `FK_RV_ID_CRENEAU` (`ID_CRENEAU`),
KEY `FK_RV_ID_CLIENT` (`ID_CLIENT`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci AUTO_INCREMENT=60 ;
يشير السطر 8 أعلاه إلى أن التركيبة [JOUR, ID_CRENEAU] يجب أن تكون فريدة، مما يمنع إدراج موعدين في نفس اليوم ضمن نفس الفترة الزمنية.
- السطر 73: نتحقق من حدوث استثناء بالفعل؛
- السطر 77: نطلب جدول مواعيد الطبيب الذي تمت إضافة موعد له للتو؛
- السطر 79: يتم التحقق من أن الموعد المضاف موجود بالفعل في جدول مواعيده؛
- السطر 82: يتم حذف الموعد الذي تمت إضافته؛
- السطر 84: نبحث في قاعدة البيانات عن الموعد الذي تم حذفه؛
- السطر 85: نتحقق من استرداد مؤشر null، مما يدل على أن الموعد المطلوب غير موجود؛
نجح تنفيذ الاختبار:
![]() |
2.10. برنامج وحدة التحكم
![]() |
برنامج وحدة التحكم بسيط. وهو يوضح كيفية استرداد مفتاح خارجي:
package rdvmedecins.boot;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
public class Boot {
// التمهيد
public static void main(String[] args) {
// إعداد التكوين
SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
app.setLogStartupInfo(false);
// تشغيلها
ConfigurableApplicationContext context = app.run(args);
// المهمة
IMetier métier = context.getBean(IMetier.class);
try {
// إضافة RV
Date jour = new Date();
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new SimpleDateFormat("dd/MM/yyyy").format(jour)));
Client client = (Client) new Client().build(1L, 1L);
Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
Rv rv = métier.ajouterRv(jour, créneau, client);
System.out.println(String.format("Rv ajouté = %s", rv));
// التحقق
créneau = métier.getCreneauById(1L);
long idMedecin = créneau.getIdMedecin();
display("Liste des rendez-vous", métier.getRvMedecinJour(idMedecin, jour));
} catch (Exception ex) {
System.out.println("Exception : " + ex.getCause());
}
// إغلاق سياق Spring
context.close();
}
// طريقة مساعدة - تعرض عناصر المجموعة
private static <T> void display(String message, Iterable<T> elements) {
System.out.println(message);
for (T element : elements) {
System.out.println(element);
}
}
}
يقوم البرنامج بإضافة موعد ثم يتحقق من أنه تمت إضافته.
- السطر 19: ستستخدم الفئة [SpringApplication] فئة التكوين [DomainAndPersistenceConfig]؛
- السطر 20: حذف سجلات بدء تشغيل التطبيق؛
- السطر 22: يتم تنفيذ الفئة [SpringApplication]. وهي تُرجع سياق Spring، أي قائمة الحبوب المسجلة؛
- السطر 24: يتم الحصول على مرجع إلى الكائن الذي ينفذ واجهة [IMetier]. وبالتالي، فهو مرجع إلى الطبقة [métier]؛
- الأسطر 27-31: إضافة موعد جديد لليوم، للعميل رقم 1 في الفترة الزمنية رقم 1. تم إنشاء العميل والفترة الزمنية من الصفر لإظهار أنه يتم استخدام المعرفات فقط. تم هنا تهيئة «الإصدار»، لكن كان من الممكن وضع أي قيمة. لا يتم استخدامها هنا؛
- السطر 34: نريد معرفة الطبيب الذي لديه الفترة الزمنية رقم 1. ولذلك، نحتاج إلى البحث في قاعدة البيانات عن الفترة الزمنية رقم 1. ونظرًا لأننا في الوضع [FetchType.LAZY]، فإن الطبيب لا يُعرض مع الفترة الزمنية. ومع ذلك، فقد حرصنا على توفير حقل [idMedecin] في الكيان [Creneau] لاسترداد المفتاح الأساسي للطبيب؛
- السطر 35: يتم استرداد «الرئيسي» الخاص بالطبيب؛
- السطر 36: يتم عرض قائمة مواعيد الطبيب؛
نتائج وحدة التحكم هي كما يلي:
2.11. مقدمة إلى Spring MVC
![]() |
ننتقل الآن إلى بناء طبقة الويب. تتكون هذه الطبقة بشكل أساسي من طرق تعالج URL محددة وترد بسطر نصي بتنسيق JSON (Javascript Object Notation). هذه الطبقة الويب هي واجهة ويب تُسمى أحيانًا «API web». سنقوم بتنفيذ هذه الواجهة باستخدام Spring MVC، وهو فرع آخر من منظومة Spring. سنبدأ بدراسة أحد الأدلة الموجودة على [http://spring.io].
2.11.1. مشروع العرض التوضيحي
![]() |
- في [1]، نقوم باستيراد أحد أدلة Spring؛
![]() |
- إلى [2]، نختار المثال [Rest Service]؛
- في [3]، نختار مشروع Maven؛
- في [4]، نأخذ النسخة النهائية من الدليل؛
- في [5]، نقوم بالتحقق من الصحة؛
- في [6]، المشروع المستورد؛
غالبًا ما تُسمى الخدمات الويب التي يمكن الوصول إليها عبر معايير URL والتي تقدم نصًا JSON بخدمات REST (REpresentational State Transfer). في هذا المستند، سأكتفي بتسمية الخدمة التي سنقوم بإنشائها بـ «خدمة ويب / JSON». تُعرف الخدمة بأنها «Restful» إذا التزمت بقواعد معينة. لم أسعَ إلى الالتزام بهذه القواعد.
لنلقِ نظرة الآن على المشروع المستورد، بدءًا من تكوين Maven الخاص به.
2.11.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.1.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-releases</id>
<url>http://repo.spring.io/release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<url>http://repo.spring.io/release</url>
</pluginRepository>
</pluginRepositories>
</project>
- الأسطر 10-14: كما هو الحال في المشروع [Spring Data]، نجد المشروع الأصلي [Spring Boot]؛
- الأسطر 17-20: يجلب الأرتيفاكت [spring-boot-starter-web] معه المكتبات اللازمة لمشروع Spring MVC. ويجلب معه على وجه الخصوص خادم Tomcat مدمجًا. وسيتم تشغيل التطبيق على هذا الخادم؛
- الأسطر 21-24: تتولى مكتبة Jackson إدارة JSON: تحويل كائن Java إلى سلسلة JSON والعكس؛
المكتبات التي توفرها هذه التهيئة عديدة جدًا:
![]() | ![]() |
فيما يلي نرى الأرشيفات الثلاثة لخادم Tomcat.
2.11.3. بنية خدمة Spring REST
تقوم خدمة Spring MVC بتنفيذ نموذج الهندسة المعمارية المعروف باسم MVC (النموذج – العرض – وحدة التحكم) بالطريقة التالية:
![]() |
تتم معالجة طلب العميل على النحو التالي:
- الطلب - تكون URL المطلوبة على شكل http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... [Dispatcher Servlet] هي فئة Spring التي تعالج URL الواردة. وهي «توجه» URL إلى الإجراء الذي يجب أن يعالجها. هذه الإجراءات هي طرق لفئات معينة تسمى [Contrôleurs]. الحرف C في MVC هو هنا السلسلة [Dispatcher Servlet, Contrôleur, Action]. إذا لم يتم تكوين أي إجراء لمعالجة URL الواردة، فسترد السيرفلت [Dispatcher Servlet] بأن URL المطلوبة لم يتم العثور عليها (خطأ 404 NOT FOUND)؛
- المعالجة
- يمكن للإجراء المختار الاستفادة من المعلمات parami التي أرسلتها إليه خدمة [Dispatcher Servlet]. وقد تأتي هذه المعلمات من عدة مصادر:
- مسار [/param1/param2/...] الخاص بـ URL،
- من المعلمات [p1=v1&p2=v2] الخاصة بـ URL,
- من المعلمات التي يرسلها المتصفح مع طلبه؛
- أثناء معالجة طلب المستخدم، قد تحتاج العملية إلى الطبقة [metier] [2b]. بمجرد معالجة طلب العميل، قد يؤدي ذلك إلى استدعاء استجابات متنوعة. ومن الأمثلة النموذجية على ذلك:
- صفحة خطأ إذا تعذر معالجة الطلب بشكل صحيح
- صفحة تأكيد في الحالات الأخرى
- تطلب الإجراء عرض طريقة عرض معينة [3]. ستعرض طريقة العرض هذه البيانات التي نسميها نموذج طريقة العرض. وهذا هو الحرف M في MVC. ستقوم الإجراء بإنشاء هذا النموذج M [2c] وستطلب عرض طريقة عرض V [3]؛
- الاستجابة - تستخدم طريقة العرض V المختارة النموذج M الذي أنشأته الإجراء لتهيئة الأجزاء الديناميكية من الاستجابة HTML التي يجب أن ترسلها إلى العميل، ثم ترسل هذه الاستجابة.
بالنسبة لخدمة ويب / JSON، يتم تعديل البنية السابقة بشكل طفيف:
![]() |
- في [4a]، يتم تحويل النموذج — وهو فئة Java — إلى سلسلة JSON بواسطة مكتبة JSON؛
- في [4b]، يتم إرسال هذه السلسلة JSON إلى المتصفح؛
2.11.4. وحدة التحكم C
![]() |
يحتوي التطبيق المستورد على وحدة التحكم التالية:
package hello;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
@RequestMapping("/greeting")
public @ResponseBody
Greeting greeting(@RequestParam(value = "name", required = false, defaultValue = "World") String name) {
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}
}
- السطر 9: التعليق التوضيحي [@Controller] يجعل الفئة [GreetingController] وحدة تحكم Spring، أي أن أساليبها مسجلة لمعالجة URL؛
- السطر 15: تشير العلامة [@RequestMapping] إلى URL الذي تعالجه الطريقة، وهو في هذه الحالة URL [/greeting]. سنرى لاحقًا أنه يمكن ضبط معلمات هذه القيمة URL وأنه من الممكن استرداد هذه المعلمات؛
- السطر 16: تشير التعليقات التوضيحية [@ResponseBody] إلى أن الطريقة لا تنتج نموذجًا لعرض (JSP، JSF، Thymeleaf، ...) يتم إرساله لاحقًا إلى متصفح العميل، بل تنتج بنفسها الاستجابة الموجهة إلى المتصفح. وهنا، تنتج كائنًا من النوع [Greeting] (السطر 18). وبشكل غير ظاهر هنا، سيتم تحويل هذا الكائن أولاً إلى JSON قبل إرساله إلى المتصفح. إن وجود مكتبة JSON ضمن تبعيات المشروع هو ما يجعل Spring Boot يقوم، عن طريق التكوين التلقائي، بتكوين المشروع بهذه الطريقة؛
- السطر 17: تحتوي الطريقة [greeting] على معلمة [String name]. تشير العلامة التوضيحية [@RequestParam(value = "name", required = false, defaultValue = "World"] إلى أن هذا المعامل يجب تهيئته باستخدام معامل يُسمى [name](@RequestParam(value = "name")). ويمكن أن يكون هذا المعلمة تابعة لمعلمة GET أو POST. هذه المعلمة ليست إلزامية (required = false). في الحالة الأخيرة، سيتم تهيئة المعلمة [name] الخاصة بالطريقة بالقيمة [World] (defaultValue = "World").
2.11.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 الناتجة عن طريقة وحدة التحكم على النحو التالي:
أو
2.11.6. تكوين المشروع
![]() |
يتم تكوين المشروع بواسطة الفئة التالية: [Application]:
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);
}
}
- السطر 11: من الغريب أن هذه الفئة قابلة للتنفيذ باستخدام طريقة [main] الخاصة بتطبيقات وحدة التحكم. وهذا هو الحال بالفعل. ستقوم الفئة [SpringApplication] في السطر 12 بتشغيل خادم Tomcat الموجود في التبعيات ونشر الخدمة REST عليه؛
- السطر 4: نلاحظ أن الفئة [SpringApplication] تنتمي إلى المشروع [Spring Boot]؛
- السطر 12: المعلمة الأولى هي الفئة التي تقوم بتكوين المشروع، والثانية هي المعلمات الإضافية (إن وجدت)؛
- السطر 8: يطلب التعليق التوضيحي [@EnableAutoConfiguration] من Spring Boot إجراء تكوين المشروع؛
- السطر 7: تعمل العلامة [@ComponentScan] على فحص المجلد الذي يحتوي على الفئة [Application] بحثًا عن مكونات Spring. سيتم العثور على مكون، وهو الفئة [GreetingController] التي تحمل التعليق التوضيحي [@Controller] الذي يجعلها مكونًا من مكونات Spring؛
2.11.7. تنفيذ المشروع
لنقم بتنفيذ المشروع:
![]() |
نحصل على سجلات وحدة التحكم التالية:
____ _ __ _ _
- السطر 12: يتم تشغيل خادم Tomcat على المنفذ 8080 (السطر 11)؛
- السطر 16: السيرفلت [DispatcherServlet] موجود؛
- السطر 19: تم اكتشاف الطريقة [GreetingController.greeting]؛
لاختبار تطبيق الويب، نطلب URL [http://localhost:8080/greeting]:
![]() | ![]() |
نتلقى بالفعل السلسلة المتوقعة JSON. قد يكون من المثير للاهتمام الاطلاع على الرؤوس HTTP المرسلة من الخادم. وللقيام بذلك، سنستخدم المكون الإضافي لمتصفح Chrome المسمى [Advanced Rest Client] (انظر الملاحق):
![]() |
- في [1]، URL المطلوب؛
- في [2]، تُستخدم الطريقة GET؛
- في [3]، الرد JSON؛
- في [4]، أشار الخادم إلى أنه يرسل استجابة بتنسيق JSON؛
- في [5]، يُطلب نفس URL ولكن هذه المرة مع POST؛
- في [7]، يتم إرسال المعلومات إلى الخادم في شكل [urlencoded]؛
- في [6]، المعلمة name مع قيمتها؛
- في [8]، يُعلم المتصفح الخادم بأنه يرسل إليه معلومات [urlencoded]؛
- في [9]، الرد JSON من الخادم؛
2.11.8. إنشاء أرشيف قابل للتنفيذ
يمكن إنشاء أرشيف قابل للتنفيذ خارج Eclipse. توجد الإعدادات اللازمة في الملف [pom.xml]:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.Application</start-class>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- تحدد الأسطر من 9 إلى 12 المكون الإضافي الذي سيقوم بإنشاء الأرشيف القابل للتنفيذ؛
- يحدد السطر 3 الفئة القابلة للتنفيذ للمشروع؛
ويتم ذلك على النحو التالي:
![]() |
- في [1]: يتم تنفيذ هدف Maven؛
- إلى [2]: هناك هدفان (goals): [clean] لحذف المجلد [target] من مشروع Maven، و[package] لإعادة إنشائه؛
- في [3]: سيتم إنشاء المجلد [target] في هذا المجلد؛
- في [4]: يتم إنشاء الملف المستهدف؛
في السجلات التي تظهر في وحدة التحكم، من المهم أن يظهر المكون الإضافي [spring-boot-maven-plugin]. فهو الذي يقوم بإنشاء الأرشيف القابل للتنفيذ.
باستخدام وحدة التحكم، ننتقل إلى المجلد الذي تم إنشاؤه:
- السطر 5: الملف المضغوط الذي تم إنشاؤه؛
يتم تشغيل هذا الملف المضغوط بالطريقة التالية:
الآن بعد تشغيل تطبيق الويب، يمكن الوصول إليه باستخدام متصفح:
![]() |
2.11.9. نشر التطبيق على خادم Tomcat
على الرغم من أن Spring Boot يُعد عمليًا للغاية في وضع التطوير، فمن المرجح أن يتم نشر التطبيق في بيئة الإنتاج على خادم 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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
....
</project>
يجب إجراء التعديلات في مكانين:
- السطر 9: يجب الإشارة إلى أنه سيتم إنشاء أرشيف war (Web ARchive)؛
- الأسطر 26-30: يجب إضافة تبعية إلى الأرتيفاكت [spring-boot-starter-tomcat]. يضيف هذا الأرتيفاكت جميع فئات Tomcat إلى تبعيات المشروع؛
- السطر 29: هذه المكونة هي [provided]، أي أن الملفات المضغوطة المقابلة لن يتم وضعها في ملف war الذي سيتم إنشاؤه. في الواقع، سيتم العثور على هذه الملفات المضغوطة على خادم Tomcat الذي سيتم تشغيل التطبيق عليه؛
كما يجب تكوين تطبيق الويب. في حالة عدم وجود ملف [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]، يتم تنفيذ المشروع على أحد الخوادم المسجلة في Eclipse؛
- في [2]، نختار [tc Server Developer] الموجود افتراضيًا. وهو نسخة معدلة من Tomcat؛
بعد ذلك، يمكننا طلب URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] في متصفح:
![]() |
أصبحنا الآن قادرين على إنشاء ملف war. بعد ذلك، سنواصل العمل مع Spring Boot وملف jar القابل للتنفيذ الخاص به.
2.11.10. إنشاء مشروع ويب جديد
لإنشاء مشروع ويب جديد، يمكننا اتباع الخطوات التالية:
![]() |
- في [1]: ملف / جديد / مشروع Spring Starter
- في [2]: حدد [Web]. لا نحدد مكتبات العروض لأن خدمة الويب / JSON لا تحتوي على عروض؛
- سيكون المشروع الذي تم إنشاؤه مشروعًا من نوع Maven. في [3]، نحدد مجموعة عنصر Maven الذي سيتم إنشاؤه، وفي [4]، نحدد اسم العنصر؛
- في [5]، نحدد اسم الحزمة التي سيضع فيها Spring فئة تكوين المشروع؛
- في [6]، نحدد اسمًا لمشروع Eclipse – قد يختلف عن [4]؛
![]() |
2.12. الطبقة [web]
![]() |
![]() |
سنقوم ببناء طبقة الويب على عدة مراحل:
- الخطوة 1: طبقة ويب جاهزة للعمل بدون مصادقة؛
- الخطوة 2: تفعيل المصادقة باستخدام Spring Security؛
- الخطوة 3: تفعيل CORS [Cross-origin resource sharing (CORS) is a mechanism that allows many resources (e.g. fonts, JavaScript, etc.) on a web page to be requested from another domain outside the domain the resource originated from. (Wikipedia)]. سيكون عميل خدمة الويب لدينا عميل ويب Angular الذي لن ينتمي بالضرورة إلى نفس المجال الذي تنتمي إليه خدمة الويب لدينا. وبالتالي، لا يمكنه الوصول إليها بشكل افتراضي ما لم تسمح له خدمة الويب بذلك. سنرى كيف؛
2.12.1. تكوين Maven
ملف [pom.xml] الخاص بالمشروع هو كما يلي:
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.mvc</groupId>
<artifactId>rdvmedecins-webapi-v1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-webapi-v1</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
- الأسطر 7-11: مشروع Maven الأصلي؛
- الأسطر 13-16: التبعيات لمشروع Spring MVC؛
- الأسطر 17-21: التبعيات الخاصة بمشروع الطبقات [métier, DAO, JPA]؛
2.12.2. واجهة خدمة الويب
![]() |
- في [1]، المذكورة أعلاه، لا يمكن للمتصفح أن يطلب سوى عدد محدود من URL باستخدام صيغة محددة؛
- في [4]، يتلقى المتصفح استجابة JSON؛
ستكون جميع ردود خدمة الويب الخاصة بنا بنفس الشكل الذي يتوافق مع تحويل JSON لكائن من النوع [Reponse] التالي:
package rdvmedecins.web.models;
public class Reponse {
// ----------------- الخصائص
// حالة العملية
private int status;
// الرد JSON
private Object data;
// ---------------المصنعون
public Reponse() {
}
public Reponse(int status, Object data) {
this.status = status;
this.data = data;
}
// الأساليب
public void incrStatusBy(int increment) {
status += increment;
}
// ----------------------أدوات الاسترجاع والتعيين
...
}
- السطر 7: رمز خطأ الرد 0: OK، أي شيء آخر: KO؛
- السطر 9: نص الرد؛
نقدم الآن لقطات الشاشة التي توضح واجهة خدمة الويب / JSON:
قائمة بجميع المرضى في العيادة الطبية [/getAllClients]
![]() |
قائمة بجميع الأطباء في العيادة الطبية [/getAllMedecins]
![]() |
قائمة المواعيد المتاحة لأحد الأطباء [/getAllCreneaux/{idMedecin}]
![]() |
قائمة مواعيد الطبيب [/getRvMedecinJour/{idMedecin}/{aaaa-mm-jj}
![]() |
جدول مواعيد الطبيب [/getAgendaMedecinJour/{idMedecin}/{aaaa-mm-jj}]
![]() |
لإضافة/حذف موعد، نستخدم ملحق Chrome [Advanced Rest Client] لأن هذه العمليات تتم باستخدام POST.
إضافة موعد [/ajouterRv]
![]() |
- في [0]، يتم استخدام URL الخاص بخدمة الويب؛
- إلى [1]، حيث تُستخدم الطريقة POST؛
- في [2]، النص JSON للمعلومات المرسلة إلى خدمة الويب في شكل {اليوم، idClient، idCreneau}؛
- في [3]، يحدد العميل لخدمة الويب أنه يرسل إليها معلومات بتنسيق JSON؛
وتكون الإجابة عندئذٍ كما يلي:
![]() |
- في [4]: يرسل العميل الرأس الذي يشير إلى أن البيانات التي يرسلها بتنسيق JSON؛
- إلى [5]: ترد خدمة الويب بأنها ترسل هي الأخرى بتنسيق JSON؛
- في [6]: الرد JSON من خدمة الويب. يحتوي الحقل [data] على النموذج JSON للموعد المضاف؛
يمكن التحقق من وجود الموعد الجديد:
![]() |
حذف موعد [/supprimerRv]
![]() |
- في [1]، يتم استخدام URL الخاص بخدمة الويب؛
- في [2]، تُستخدم الطريقة POST؛
- في [3]، النص JSON للمعلومات المرسلة إلى خدمة الويب في شكل {idRv}؛
- في [4]، يحدد العميل لخدمة الويب أنه يرسل إليها المعلومات JSON؛
وتكون الإجابة عندئذٍ كما يلي:
![]() |
- في [5]: الحقل [status] يساوي 0، مما يدل على نجاح العملية؛
يمكن التحقق من حذف الموعد:
![]() |
فيما سبق، لم يعد موعد المريض [Mme GERMAN] موجودًا.
تتيح خدمة الويب أيضًا استرداد الكيانات عبر معرّفاتها:
![]() |
![]() |
![]() |
![]() |
يتم معالجة جميع هذه الكيانات URL بواسطة وحدة التحكم [RdvMedecinsController] التي نقدمها الآن.
2.12.3. الهيكل الأساسي لوحدة التحكم [RdvMedecinsController]
![]() |
وحدة التحكم [RdvMedecinsController] هي كما يلي:
package rdvmedecins.web.controllers;
import java.text.ParseException;
...
@RestController
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
private List<String> messages;
@PostConstruct
public void init() {
// رسائل خطأ التطبيق
messages = application.getMessages();
}
// قائمة الأطباء
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
public Reponse getAllMedecins() {
...
}
// قائمة العملاء
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
public Reponse getAllClients() {
...
}
// قائمة المواعيد المتاحة للطبيب
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
...
}
// قائمة مواعيد الطبيب
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin,
@PathVariable("jour") String jour) {
...
}
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
public Reponse getClientById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
public Reponse getMedecinById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
public Reponse getRvById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
public Reponse getCreneauById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
...
}
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
...
}
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getAgendaMedecinJour(
@PathVariable("idMedecin") long idMedecin,
@PathVariable("jour") String jour) {
...
}
}
- السطر 6: التعليق التوضيحي [@RestController] يجعل الفئة [RdvMedecinsController] وحدة تحكم Spring. كما أنها تؤدي أيضًا إلى أن الطرق التي تعالج URL ستولد استجابة سيتم تحويلها تلقائيًا إلى JSON؛
- السطران 9-10: سيتم حقن كائن من النوع [ApplicationModel] هنا بواسطة Spring؛
- السطر 13: تُشير العلامة التوضيحية [@PostConstruct] إلى طريقة يجب تنفيذها مباشرة بعد إنشاء مثيل للفئة. وعند تنفيذها، تكون الكائنات التي حقنها Spring متاحة؛
- تُرجع جميع الطرق كائنًا من النوع [Reponse] كما يلي:
package rdvmedecins.web.models;
public class Reponse {
// ----------------- الخصائص
// حالة العملية
private int status;
// الرد
private Object data;
...
}
يتم تسلسل هذا الكائن إلى JSON قبل إرساله إلى متصفح العميل؛
- السطر 20: تحدد العلامة التوضيحية [@RequestMapping] شروط استدعاء الأسلوب. هنا، يعالج الأسلوب طلبًا GET من URL [/getAllMedecins]. وإذا تم طلب هذه الطريقة URL من قبل طريقة POST، فسيتم رفضها وسيقوم Spring MVC بإرسال رمز خطأ HTTP إلى عميل الويب؛
- السطر 32: يتم تعيين معلمة URL بواسطة {idMedecin}. يتم استرداد هذه المعلمة باستخدام التعليق التوضيحي [@PathVariable] في السطر 33؛
- السطر 33: المعلمة الوحيدة [long idMedecin] تستمد قيمتها من المعلمة {idMedecin} في URL [@PathVariable("idMedecin")]. قد تحمل المعلمة في URL والمعلمة في الطريقة أسماء مختلفة. تجدر الإشارة هنا إلى أن [@PathVariable("idMedecin")] من نوع String (جميع عناصر URL هي من نوع String) في حين أن المعلمة [long idMedecin] من نوع [long]. يتم تغيير النوع تلقائيًا. يتم إرجاع رمز خطأ HTTP في حالة فشل تغيير النوع هذا؛
- السطر 65: يشير التعليق التوضيحي [@RequestBody] إلى نص الاستعلام. في طلب GET، لا يوجد نص أبدًا تقريبًا (ولكن من الممكن إدراج واحد). أما في طلب POST، فغالبًا ما يكون هناك نص (ولكن من الممكن عدم إدراجه). بالنسبة لـ URL و [ajouterRv]، يرسل عميل الويب في POST الخاص به السلسلة التالية:
تؤدي صيغة [@RequestBody PostAjouterRv post] (السطر 65) بالإضافة إلى أن الطريقة تتوقع JSON [consumes = "application/json; charset=UTF-8"] في السطر 64، سيؤدي ذلك إلى تحويل السلسلة JSON المرسلة من عميل الويب إلى كائن من النوع [PostAjouter]. وهذا الكائن هو التالي:
package rdvmedecins.web.models;
public class PostAjouterRv {
// بيانات المنشور
private String jour;
private long idClient;
private long idCreneau;
// دالات الاسترجاع والتعيين
...
}
وهنا أيضًا، ستتم التغييرات اللازمة في النوع تلقائيًا؛
- في السطرين 69-70، نجد آلية مماثلة لـ URL و [/supprimerRv]. السلسلة JSON المرسلة هي كما يلي:
والنوع [PostSupprimerRv] هو التالي:
package rdvmedecins.web.models;
public class PostSupprimerRv {
// بيانات الإرسال
private long idRv;
// دالات الاسترجاع والتعيين
...
}
2.12.4. نماذج خدمة الويب
![]() |
لقد قدمنا سابقًا الطرازات [Reponse, PostAjouterRv, PostSupprimerRv]. أما الطراز [ApplicationModel] فهو كما يلي:
package rdvmedecins.web.models;
import java.util.Date;
...
@Component
public class ApplicationModel implements IMetier {
// الطبقة [métier]
@Autowired
private IMetier métier;
// البيانات الواردة من الطبقة [métier]
private List<Medecin> médecins;
private List<Client> clients;
// رسائل الخطأ
private List<String> messages;
@PostConstruct
public void init() {
// يتم استرداد الأطباء والعملاء
try {
médecins = métier.getAllMedecins();
clients = métier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
// مُستخرج
public List<String> getMessages() {
return messages;
}
// ------------------------- واجهة طبقة [métier]
@Override
public List<Client> getAllClients() {
return clients;
}
@Override
public List<Medecin> getAllMedecins() {
return médecins;
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return métier.getAllCreneaux(idMedecin);
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return métier.getRvMedecinJour(idMedecin, jour);
}
@Override
public Client getClientById(long id) {
return métier.getClientById(id);
}
@Override
public Medecin getMedecinById(long id) {
return métier.getMedecinById(id);
}
@Override
public Rv getRvById(long id) {
return métier.getRvById(id);
}
@Override
public Creneau getCreneauById(long id) {
return métier.getCreneauById(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
return métier.ajouterRv(jour, creneau, client);
}
@Override
public void supprimerRv(Rv rv) {
métier.supprimerRv(rv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
return métier.getAgendaMedecinJour(idMedecin, jour);
}
}
- السطر 6: التعليق التوضيحي [@Component] يجعل من الفئة [ApplicationModel] مكونًا من مكونات Spring. وكما هو الحال مع جميع مكونات Spring التي رأيناها حتى الآن (باستثناء @Controller)، سيتم إنشاء مثيل واحد فقط من هذا النوع (singleton)؛
- السطر 7: الفئة [ApplicationModel] تُنفذ الواجهة [IMetier]؛
- السطران 10-11: يتم حقن مرجع على الطبقة [métier] بواسطة Spring؛
- السطر 19: يؤدي التعليق التوضيحي [@PostConstruct] إلى تنفيذ الطريقة [init] مباشرةً بعد إنشاء مثيل للفئة [ApplicationModel]؛
- السطران 23-24: يتم استرداد قوائم الأطباء والعملاء من الطبقة [métier]؛
- السطر 26: في حالة حدوث استثناء، يتم تخزين رسائل مكدس الاستثناءات في الحقل الموجود في السطر 17؛
ستفيدنا الفئة [ApplicationModel] في أمرين:
- كذاكرة تخزين مؤقتة لتخزين قوائم الأطباء والمرضى (العملاء)؛
- كواجهة موحدة لوحدات التحكم؛
تتطور بنية طبقة الويب على النحو التالي:
![]() |
- في [2b]، تتواصل أساليب وحدة التحكم (أو وحدات التحكم) مع العنصر الفريد [ApplicationModel]؛
توفر هذه الاستراتيجية مرونة في إدارة ذاكرة التخزين المؤقت. حاليًا، لا يتم تخزين مواعيد الأطباء في ذاكرة التخزين المؤقت. ولإدراجها فيها، يكفي تعديل الفئة [ApplicationModel]. ولا يؤثر ذلك بأي شكل على وحدة التحكم التي ستستمر في استخدام الطريقة [List<Creneau> getAllCreneaux(long idMedecin)] كما كانت تفعل من قبل. ما سيتم تغييره هو تنفيذ هذه الطريقة في [ApplicationModel].
2.12.5. الفئة Static
تضم الفئة [Static] مجموعة من الطرق الثابتة المساعدة التي لا تتعلق بـ«الأعمال» أو «الويب»:
![]() |
وإليك كودها:
package rdvmedecins.web.helpers;
import java.text.SimpleDateFormat;
...
public class Static {
public Static() {
}
// قائمة رسائل الخطأ الخاصة باستثناء
public static List<String> getErreursForException(Exception exception) {
// يتم استرداد قائمة رسائل الخطأ الخاصة بالاستثناء
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
erreurs.add(cause.getMessage());
cause = cause.getCause();
}
return erreurs;
}
// أدوات التعيين Object --> Map
// --------------------------------------------------------
....
}
- السطر 12: الأسلوب [Static.getErreursForException] الذي تم استخدامه (السطر 8 أدناه) في الأسلوب [init] للفئة [ApplicationModel]:
@PostConstruct
public void init() {
// استرداد الأطباء والعملاء
try {
médecins = métier.getAllMedecins();
clients = métier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
تقوم هذه الطريقة بإنشاء كائن [List<String>] مع رسائل الخطأ [exception.getMessage()] الخاصة باستثناء [exception] وتلك التي يحتوي عليها [exception.getCause()].
تحتوي الفئة [Static] على طرق مساعدة أخرى سنعود إليها عند ذكرها لاحقًا.
سنقوم الآن بتفصيل معالجة URL لخدمة الويب. هناك ثلاث فئات رئيسية معنية بهذه المعالجة:
- وحدة التحكم [RdvMedecinsController]؛
- فئة الطرق المساعدة [Static]؛
- فئة ذاكرة التخزين المؤقت [ApplicationModel]؛
![]() |
2.12.6. الطريقة [init] الخاصة بوحدة التحكم
تحتوي وحدة التحكم [RdvMedecinsController] (انظر الفقرة 2.12.3) على طريقة [init] التي يتم تنفيذها مباشرة بعد إنشاء مثيل لها:
@Autowired
private ApplicationModel application;
private List<String> messages;
@PostConstruct
public void init() {
// رسائل خطأ التطبيق
messages = application.getMessages();
}
- السطر 8: يتم تخزين رسائل الخطأ الموجودة في ذاكرة التخزين المؤقتة للتطبيق [ApplicationModel] محليًّا في حقل السطر 3. وهذا سيسمح للطرق بمعرفة ما إذا كان التطبيق قد تم تهيئته بشكل صحيح.
2.12.7. يتم معالجة URL [/getAllMedecins]
يتم معالجة URL [/getAllMedecins] بواسطة الطريقة التالية في وحدة التحكم [RdvMedecinsController]:
// قائمة الأطباء
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
public Reponse getAllMedecins() {
// حالة التطبيق
if (messages != null) {
return new Reponse(-1, messages);
}
// قائمة الأطباء
try {
return new Reponse(0, application.getAllMedecins());
} catch (Exception e) {
return new Reponse(1, Static.getErreursForException(e));
}
}
- السطر 5: يتم التحقق مما إذا كان التطبيق قد تم تهيئته بشكل صحيح (messages==null). إذا لم يكن الأمر كذلك، يتم إرجاع استجابة تحتوي على status=-1 و data=messages؛
- السطر 10: وإلا، يتم إرجاع قائمة الأطباء مع status يساوي 0. لا تطلق الطريقة [application.getAllMedecins()] استثناءً لأنها تكتفي بإرجاع قائمة موجودة في ذاكرة التخزين المؤقت. ومع ذلك، سنحتفظ بإدارة الاستثناءات هذه تحسبًا لحالة عدم وجود الأطباء في ذاكرة التخزين المؤقت؛
لم نوضح بعد الحالة التي يتم فيها تهيئة التطبيق بشكل خاطئ. دعونا نوقف SGBD و MySQL5، ونشغل خدمة الويب، ثم نطلب URL و [/getAllMedecins]:

نحصل بالفعل على خطأ. في الظروف العادية، نحصل على العرض التالي:
![]() |
2.12.8. يتم معالجة URL [/getAllClients]
يتم معالجة URL [/getAllClients] بواسطة الطريقة التالية في وحدة التحكم [RdvMedecinsController]:
// قائمة العملاء
@RequestMapping(value = "/getAllClients")
public Reponse getAllClients() {
// حالة التطبيق
if (messages != null) {
return new Reponse(-1, messages);
}
// قائمة العملاء
try {
return new Reponse(0, application.getAllClients());
} catch (Exception e) {
return new Reponse(1, Static.getErreursForException(e));
}
}
وهي مشابهة للطريقة [getAllMedecins] التي تمت دراستها سابقًا. والنتائج التي تم الحصول عليها هي كما يلي:
![]() |
2.12.9. تتم معالجة URL و [/getAllCreneaux/{idMedecin}]
يتم معالجة URL و [/getAllCreneaux/{idMedecin}] بواسطة الطريقة التالية الخاصة بوحدة التحكم [RdvMedecinsController]:
// قائمة المواعيد المتاحة لكل طبيب
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
// حالة التطبيق
if (messages != null) {
return new Reponse(-1, messages);
}
// استرداد الطبيب
Reponse réponse = getMedecin(idMedecin);
if (réponse.getStatus() != 0) {
return réponse;
}
Medecin médecin = (Medecin) réponse.getData();
// المواعيد المتاحة للطبيب
List<Creneau> créneaux = null;
try {
créneaux = application.getAllCreneaux(médecin.getId());
} catch (Exception e1) {
return new Reponse(3, Static.getErreursForException(e1));
}
// إرجاع الرد
return new Reponse(0, Static.getListMapForCreneaux(créneaux));
}
- السطر 9: يُطلب من الطبيب المحدد بالمعلمة [id] استدعاء طريقة محلية:
private Reponse getMedecin(long id) {
// استرداد بيانات الطبيب
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// هل هذا الطبيب موجود؟
if (médecin == null) {
return new Reponse(2, null);
}
// حسناً
return new Reponse(0, médecin);
}
يتم العودة من هذه الطريقة بقيمة status في [0,1,2]. لنعد إلى كود الطريقة [getAllCreneaux]:
- الأسطر 10-12: إذا كانت القيمة هي status!=0، يتم إرجاع الاستجابة على الفور؛
- السطر 13: يتم استدعاء الطبيب؛
- السطر 17: نسترد المواعيد المتاحة لهذا الطبيب؛
- السطر 22: إرسال كائن [Static.getListMapForCreneaux(créneaux)] كرد؛
لنتذكر تعريف الفئة [Creneau]:
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// خصائص موعد RV
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// الفترة الزمنية مرتبطة بطبيب
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// مفتاح خارجي
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
...
}
- السطر 13: يتم البحث عن الطبيب في وضع [FetchType.LAZY]؛
لنتذكر الاستعلام JPQL الذي ينفذ الطريقة [getAllCreneaux] في الطبقة [DAO]:
@Query("select c from Creneau c where c.medecin.id=?1")
تفرض الصيغة [c.medecin.id] إجراء عملية ربط بين الجدولين [CRENEAUX] و [MEDECINS]. وبالتالي، فإن الاستعلام يعرض جميع المواعيد المتاحة للطبيب مع ذكر اسم الطبيب في كل منها. وعند تسلسل هذه المواعيد في JSON، تظهر سلسلة JSON الخاصة بالطبيب في كل منها. وهذا أمر غير ضروري. لذلك، بدلاً من تسلسل كائن [Creneau]، سنقوم بتسلسل كائن [Map] الذي سنضع فيه الحقول المطلوبة فقط.
لنعد إلى الكود الذي درسناه في البداية:
// يتم إرجاع الرد
return new Reponse(0, Static.getListMapForCreneaux(créneaux));
الطريقة [Static.getListMapForCreneaux] هي كما يلي:
// List<Creneau> --> List<Map>
public static List<Map<String, Object>> getListMapForCreneaux(List<Creneau> créneaux) {
// قائمة من القواميس <String,Object>
List<Map<String, Object>> liste = new ArrayList<Map<String, Object>>();
for (Creneau créneau : créneaux) {
liste.add(Static.getMapForCreneau(créneau));
}
// يتم إرجاع القائمة
return liste;
}
والطريقة [Static.getMapForCreneau] هي كما يلي:
// Creneau --> Map
public static Map<String, Object> getMapForCreneau(Creneau créneau) {
// هل هناك شيء يجب فعله؟
if (créneau == null) {
return null;
}
// قاموس <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", créneau.getId());
hash.put("hDebut", créneau.getHdebut());
hash.put("mDebut", créneau.getMdebut());
hash.put("hFin", créneau.getHfin());
hash.put("mFin", créneau.getMfin());
// نقوم بتحويل القاموس
return hash;
}
- السطر 8: يتم إنشاء قاموس؛
- الأسطر 9-13: نضع فيه الحقول التي نريد الاحتفاظ بها في السلسلة JSON. الحقل [medecin] غير موجود فيه؛
- السطر 15: يتم إرجاع هذا القاموس؛
النتائج التي تم الحصول عليها هي التالية:
![]() |
أو هذه النتائج إذا لم يكن النطاق موجودًا:
![]() |
أو هذه النتائج في حالة حدوث خطأ في الوصول إلى قاعدة البيانات:
![]() |
2.12.10. يتم معالجة URL [/getRvMedecinJour/{idMedecin}/{jour}]
يتم معالجة URL و [/getRvMedecinJour/{idMedecin}/{jour}] بالطريقة التالية الخاصة بوحدة التحكم [RdvMedecinsController]:
// قائمة مواعيد الطبيب
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
// حالة التطبيق
if (messages != null) {
return new Reponse(-1, messages);
}
// التحقق من التاريخ
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(3, null);
}
// استرداد الطبيب
Reponse réponse = getMedecin(idMedecin);
if (réponse.getStatus() != 0) {
return réponse;
}
Medecin médecin = (Medecin) réponse.getData();
// قائمة مواعيده
List<Rv> rvs = null;
try {
rvs = application.getRvMedecinJour(médecin.getId(), jourAgenda);
} catch (Exception e1) {
return new Reponse(4, Static.getErreursForException(e1));
}
// إرجاع الرد
return new Reponse(0, Static.getListMapForRvs(rvs));
}
- السطر 31: يتم إرجاع كائن List<Map<String,Object>> بدلاً من كائن List<Rv>. لنستذكر تعريف الفئة [Rv]:
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// خصائص الموعد
@Temporal(TemporalType.DATE)
private Date jour;
// موعد مرتبط بعميل
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// موعد مرتبط بفترة زمنية
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// المفاتيح الخارجية
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
...
}
- السطر 11: يتم البحث عن العميل باستخدام الوضع [FetchType.LAZY]؛
- السطر 18: يتم البحث عن الفترة الزمنية باستخدام الوضع [FetchType.LAZY]؛
لنتذكر الاستعلام JPQL الذي يبحث عن المواعيد:
@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
يتم إجراء عمليات ربط صريحة لاسترجاع الحقول [client] و [creneau]. من ناحية أخرى، وبسبب عملية الربط [cr.medecin.id=?1]، سنحصل أيضًا على اسم الطبيب. وبالتالي سيظهر اسم الطبيب في السلسلة JSON لكل موعد. لكن هذه المعلومات المكررة غير مفيدة. لنعد إلى كود الأسلوب:
- السطر 31: نقوم بأنفسنا بإنشاء القاموس المراد تسلسله في JSON؛
القاموس الذي تم إنشاؤه لموعد ما هو كما يلي:
// موعد --> الخريطة
public static Map<String, Object> getMapForRv(Rv rv) {
// هل هناك ما يجب فعله؟
if (rv == null) {
return null;
}
// قاموس <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", rv.getId());
hash.put("client", rv.getClient());
hash.put("creneau", getMapForCreneau(rv.getCreneau()));
// نقوم بإرجاع القاموس
return hash;
}
- السطر 11: نستعيد القاموس الخاص بالكائن [Creneau] الذي عرضناه سابقًا؛
النتائج التي تم الحصول عليها هي كما يلي:
![]() |
أو هذه النتائج التي تحتوي على تاريخ غير صحيح:
![]() |
أو هذه النتائج التي تحتوي على اسم طبيب غير صحيح:
![]() |
2.12.11. يتم معالجة URL و [/getAgendaMedecinJour/{idMedecin}/{jour}]
يتم معالجة URL و [/getAgendaMedecinJour/{idMedecin}/{jour}] بالطريقة التالية من وحدة التحكم [RdvMedecinsController]:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
// حالة التطبيق
if (messages != null) {
return new Reponse(-1, messages);
}
// نتحقق من التاريخ
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(3, new String[] { String.format("jour [%s] invalide", jour) });
}
// استرداد معلومات الطبيب
Reponse réponse = getMedecin(idMedecin);
if (réponse.getStatus() != 0) {
return réponse;
}
Medecin médecin = (Medecin) réponse.getData();
// يتم استرداد جدول مواعيده
AgendaMedecinJour agenda = null;
try {
agenda = application.getAgendaMedecinJour(médecin.getId(), jourAgenda);
} catch (Exception e1) {
return new Reponse(4, Static.getErreursForException(e1));
}
// حسناً
return new Reponse(0, Static.getMapForAgendaMedecinJour(agenda));
}
}
- في السطر 30، يتم إرجاع كائن من النوع List<Map<String,Object>.
الطريقة [Static.getMapForAgendaMedecinJour] هي كما يلي:
// AgendaMedecinJour --> الخريطة
public static Map<String, Object> getMapForAgendaMedecinJour(AgendaMedecinJour agenda) {
// هل هناك شيء يجب فعله؟
if (agenda == null) {
return null;
}
// قاموس <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("medecin", agenda.getMedecin());
hash.put("jour", new SimpleDateFormat("yyyy-MM-dd").format(agenda.getJour()));
List<Map<String, Object>> créneaux = new ArrayList<Map<String, Object>>();
for (CreneauMedecinJour créneau : agenda.getCreneauxMedecinJour()) {
créneaux.add(getMapForCreneauMedecinJour(créneau));
}
hash.put("creneauxMedecin", créneaux);
// نقوم بتحويل القاموس
return hash;
}
يحتوي القاموس الذي تم إنشاؤه على ثلاثة حقول:
- [medecin]: الطبيب صاحب الأجندة. تم الاحتفاظ بهذه المعلومة لأنها لا تظهر إلا مرة واحدة، بينما في الحالات السابقة كانت تتكرر في كل سلسلة JSON؛
- [jour]: يوم التقويم؛
- [creneauxMedecin]: قائمة فترات عمل الطبيب مع أي موعد محتمل في تلك الفترة؛
الطريقة [getMapForCreneauMedecinJour] المستخدمة في السطر 13 هي كما يلي:
// CreneauMedecinJour --> خريطة
public static Map<String, Object> getMapForCreneauMedecinJour(CreneauMedecinJour créneau) {
// هل هناك شيء يجب فعله؟
if (créneau == null) {
return null;
}
// القاموس <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("creneau", getMapForCreneau(créneau.getCreneau()));
hash.put("rv", getMapForRv(créneau.getRv()));
// نقوم بإرجاع القاموس
return hash;
}
- السطران 9-10: يتم استخدام القواميس التي تمت دراستها سابقًا لأنواع [Creneau] و [Rv]، والتي لا تحتوي بالتالي على كائن [Medecin]؛
النتائج التي تم الحصول عليها هي كما يلي:
![]() |
أو هذه النتائج إذا كان اليوم غير صحيح:
![]() |
أو هذه النتائج إذا كان رقم الطبيب غير صالح:
![]() |
2.12.12. يتم معالجة URL [/getMedecinById/{id}]
يتم معالجة URL و [/getMedecinById/{id}] بالطريقة التالية من وحدة التحكم [RdvMedecinsController]:
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
public Reponse getMedecinById(@PathVariable("id") long id) {
// حالة التطبيق
if (messages != null) {
return new Reponse(-1, messages);
}
// نسترد الطبيب
return getMedecin(id);
}
السطر 8، الطريقة [getMedecin] هي كما يلي:
private Reponse getMedecin(long id) {
// استرداد الطبيب
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// هل الطبيب موجود؟
if (médecin == null) {
return new Reponse(2, null);
}
// حسناً
return new Reponse(0, médecin);
}
النتائج التي تم الحصول عليها هي كما يلي:
![]() |
أو هذه النتائج إذا كان رقم الطبيب غير صحيح:
![]() |
2.12.13. يتم معالجة URL [/getClientById/{id}]
يتم معالجة URL [/getClientById/{id}] بالطريقة التالية من وحدة التحكم [RdvMedecinsController]:
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
public Reponse getClientById(@PathVariable("id") long id) {
// حالة التطبيق
if (messages != null) {
return new Reponse(-1, messages);
}
// جاري استرداد العميل
return getClient(id);
}
السطر 8، الطريقة [getClient] هي كما يلي:
private Reponse getClient(long id) {
// يتم استرداد العميل
Client client = null;
try {
client = application.getClientById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// هل العميل موجود؟
if (client == null) {
return new Reponse(2, null);
}
// حسناً
return new Reponse(0, client);
}
والنتائج التي تم الحصول عليها هي كما يلي:
![]() |
أو هذه النتائج إذا كان رقم العميل غير صحيح:
![]() |
2.12.14. يتم معالجة URL و [/getCreneauById/{id}]
يتم معالجة URL و [/getCreneauById/{id}] بالطريقة التالية من وحدة التحكم [RdvMedecinsController]:
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
public Reponse getCreneauById(@PathVariable("id") long id) {
// حالة التطبيق
if (messages != null) {
return new Reponse(-1, messages);
}
// يتم استرداد الموعد
Reponse réponse = getCreneau(id);
if (réponse.getStatus() == 0) {
réponse.setData(Static.getMapForCreneau((Creneau) réponse.getData()));
}
// النتيجة
return réponse;
}
السطر 8، الطريقة [getCreneau] هي كما يلي:
private Reponse getCreneau(long id) {
// جاري استرداد الموعد
Creneau créneau = null;
try {
créneau = application.getCreneauById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// هل الفترة الزمنية موجودة؟
if (créneau == null) {
return new Reponse(2, null);
}
// حسناً
return new Reponse(0, créneau);
}
والنتائج التي تم الحصول عليها هي كما يلي:
![]() |
أو هذه النتائج إذا كان رقم الفترة الزمنية غير صحيح:
![]() |
2.12.15. يتم معالجة URL [/getRvById/{id}]
يتم معالجة URL [/getRvById/{id}] بالطريقة التالية من وحدة التحكم [RdvMedecinsController]:
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
public Reponse getRvById(@PathVariable("id") long id) {
// حالة التطبيق
if (messages != null) {
return new Reponse(-1, messages);
}
// يتم استرداد موعد اللقاء
Reponse réponse = getRv(id);
if (réponse.getStatus() == 0) {
réponse.setData(Static.getMapForRv2((Rv) réponse.getData()));
}
// النتيجة
return réponse;
}
السطر 8، الطريقة [getRv] هي كما يلي:
private Reponse getRv(long id) {
// يتم استرداد Rv
Rv rv = null;
try {
rv = application.getRvById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// هل Rv موجود؟
if (rv == null) {
return new Reponse(2, null);
}
// حسنًا
return new Reponse(0, rv);
}
السطر 10، الطريقة [Static.getMapForRv2] هي كما يلي:
// Rv --> الخريطة
public static Map<String, Object> getMapForRv2(Rv rv) {
// هل هناك ما يجب فعله؟
if (rv == null) {
return null;
}
// قاموس <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", rv.getId());
hash.put("idClient", rv.getIdClient());
hash.put("idCreneau", rv.getIdCreneau());
// نقوم بإرجاع القاموس
return hash;
}
النتائج التي تم الحصول عليها هي كما يلي:
![]() |
أو هذه النتائج إذا كان رقم الموعد غير صحيح:
![]() |
2.12.16. يتم معالجة URL [/ajouterRv]
يتم معالجة URL [/ajouterRv] بالطريقة التالية من وحدة التحكم [RdvMedecinsController]:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
// حالة التطبيق
if (messages != null) {
return new Reponse(-1, messages);
}
// نسترد القيم المرسلة
String jour = post.getJour();
long idCreneau = post.getIdCreneau();
long idClient = post.getIdClient();
// نتحقق من التاريخ
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(6, null);
}
// استرداد الفترة الزمنية
Reponse réponse = getCreneau(idCreneau);
if (réponse.getStatus() != 0) {
return réponse;
}
Creneau créneau = (Creneau) réponse.getData();
// استرداد العميل
réponse = getClient(idClient);
if (réponse.getStatus() != 0) {
réponse.incrStatusBy(2);
return réponse;
}
Client client = (Client) réponse.getData();
// إضافة الموعد
Rv rv = null;
try {
rv = application.ajouterRv(jourAgenda, créneau, client);
} catch (Exception e1) {
return new Reponse(5, Static.getErreursForException(e1));
}
// يتم إرجاع الرد
return new Reponse(0, Static.getMapForRv(rv));
}
لا يوجد هنا ما لم نره من قبل. في السطر 41، يتم إرجاع الموعد الذي تمت إضافته في السطر 36.
تبدو النتائج التي تم الحصول عليها كما يلي مع العميل [Advanced Rest Client]:
![]() |
أو هكذا إذا تم إدخال رقم فترة زمنية غير موجودة على سبيل المثال:
![]() |
![]() |
2.12.17. يتم معالجة URL [/supprimerRv]
يتم معالجة URL [/supprimerRv] بالطريقة التالية من وحدة التحكم [RdvMedecinsController]:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
// حالة التطبيق
if (messages != null) {
return new Reponse(-1, messages);
}
// استرداد القيم المرسلة
long idRv = post.getIdRv();
// استرداد rv
Reponse réponse = getRv(idRv);
if (réponse.getStatus() != 0) {
return réponse;
}
// حذف rv
try {
application.supprimerRv(idRv);
} catch (Exception e1) {
return new Reponse(3, Static.getErreursForException(e1));
}
// حسناً
return new Reponse(0, null);
}
النتائج التي تم الحصول عليها هي كما يلي:
![]() |
أو هذه النتائج في حالة عدم وجود رقم الموعد:
![]() |
لقد انتهينا من وحدة التحكم. سنرى الآن كيفية تكوين المشروع.
2.12.18. تكوين خدمة الويب
![]() |
فئة التكوين [AppConfig] هي كما يلي:
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class })
public class AppConfig {
}
- السطر 9: ننتقل إلى وضع [AutoConfiguration] حتى يتمكن Spring Boot من تهيئة المشروع وفقًا للأرشيفات التي سيجدها في مسار الفئات (Classpath) الخاص بالمشروع؛
- السطر 10: نطلب البحث عن مكونات Spring في الحزمة [rdvmedecins.web] وفروعها. وبهذه الطريقة سيتم اكتشاف المكونات:
- [@RestController RdvMedecinsController] في الحزمة [rdvmedecins.web.controllers]؛
- [@Component ApplicationModel] في الحزمة [rdvmedecins.web.models]؛
- السطر 11: يتم استيراد الفئة [DomainAndPersistenceConfig] التي تهيئ المشروع [rdvmedecins-metier-dao] من أجل الوصول إلى مكونات هذا المشروع؛
2.12.19. الفئة القابلة للتنفيذ لخدمة الويب
![]() |
الفئة [Boot] هي كما يلي:
package rdvmedecins.web.boot;
import org.springframework.boot.SpringApplication;
import rdvmedecins.web.config.AppConfig;
public class Boot {
public static void main(String[] args) {
SpringApplication.run(AppConfig.class, args);
}
}
في السطر 10، يتم تنفيذ الطريقة الثابتة [SpringApplication.run] مع الفئة [AppConfig] الخاصة بتكوين المشروع كمعلمة أولى. ستقوم هذه الطريقة بالتكوين التلقائي للمشروع، وتشغيل خادم Tomcat المدمج في التبعيات، ونشر وحدة التحكم [RdvMedecinsController] عليه.
فيما يلي سجلات التنفيذ:
- السطر 17: بدء تشغيل خادم Tomcat؛
- الأسطر 23-31: يتم تهيئة طبقات [métier, DAO, JPA]؛
- السطر 34: تم اكتشاف الطريقة التي تعالج URL [/getRvMedecinJour/{idMedecin}/{jour}]. تتكرر عملية اكتشاف طرق وحدة التحكم هذه حتى السطر 44؛
- السطر 52: أصبحت خدمة Spring MVC [DispatcherServlet] جاهزة للاستجابة لطلبات عملاء الويب؛
لدينا الآن خدمة ويب جاهزة للعمل ويمكن الوصول إليها عبر متصفح ويب. سننتقل الآن إلى تأمين هذه الخدمة: نريد أن يقتصر إدارة مواعيد الأطباء على أشخاص معينين فقط. وسنستخدم لهذا الغرض إطار عمل Spring Security، وهو أحد مكونات منظومة Spring.
2.13. مقدمة إلى Spring Security
سنقوم مرة أخرى باستيراد دليل Spring باتباع الخطوات من 1 إلى 3 أدناه:
![]() |
![]() |
يتكون المشروع من العناصر التالية:
- في المجلد [templates]، توجد صفحات HTML الخاصة بالمشروع؛
- [Application]: هي الفئة القابلة للتنفيذ للمشروع؛
- [MvcConfig]: هي فئة تكوين Spring MVC؛
- [WebSecurityConfig]: هي فئة تكوين Spring Security؛
2.13.1. تكوين Maven
المشروع [3] هو مشروع Maven. دعونا نلقي نظرة على ملفه [pom.xml] لمعرفة تبعياته:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
- الأسطر 1-5: المشروع هو مشروع Spring Boot؛
- الأسطر 8-11: يعتمد على إطار العمل [Thymeleaf] الذي يسمح بإنشاء صفحات HTML ديناميكية. يمكن لهذا الإطار أن يحل محل صفحات JSP (Java Server Pages) التي كانت حتى وقت قريب هي الإطار الافتراضي لعرض Spring MVC؛
- الأسطر 12-15: الاعتماد على إطار عمل Spring Security؛
2.13.2. طرق عرض Thymeleaf
![]() |
تبدو طريقة العرض [home.html] كما يلي:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click <a th:href="@{/hello}">here</a> to see a greeting.
</p>
</body>
</html>
- السمات [th:xx] هي سمات Thymeleaf. يتم تفسيرها بواسطة Thymeleaf قبل إرسال الصفحة HTML إلى العميل. ولا يراها العميل؛
- السطر 12: ستؤدي السمة [th:href="@{/hello}"] إلى إنشاء السمة [href] لعلامة <a>. ستؤدي القيمة [@{/hello}] إلى إنشاء المسار [<context>/hello]، حيث [context] هو سياق تطبيق الويب؛
الرمز HTML الذي تم إنشاؤه هو التالي:
- السطر 10: سياق التطبيق هو الجذر /؛
الطريقة [hello.html] هي كما يلي:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
</body>
</html>
- السطر 9: ستقوم السمة [th:inline="text"] بإنشاء نص العلامة <h1>. يحتوي هذا النص على تعبير $ يجب تقييمه. العنصر [[${#httpServletRequest.remoteUser}]] هو قيمة السمة [RemoteUser] للاستعلام HTTP الحالي. وهو اسم المستخدم المسجل الدخول؛
- السطر 10: نموذج HTML. سيؤدي السمة [th:action="@{/logout}"] إلى إنشاء السمة [action] لعلامة [form]. ستؤدي القيمة [@{/logout}] إلى إنشاء المسار [<context>/logout]، حيث يمثل [context] سياق تطبيق الويب؛
الرمز HTML الذي تم إنشاؤه هو التالي:
- السطر 8: ترجمة Hello [[${#httpServletRequest.remoteUser}]]!؛
- السطر 9: ترجمة @{/logout}؛
- السطر 11: حقل مخفي يُسمى (السمة name) _csrf؛
الطريقة الأخيرة [login.html] هي كما يلي:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<div th:if="${param.error}">Invalid username and password.</div>
<div th:if="${param.logout}">You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<label> User Name : <input type="text" name="username" />
</label>
</div>
<div>
<label> Password: <input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Sign In" />
</div>
</form>
</body>
</html>
- السطر 9: السمة [th:if="${param.error}"] تجعل علامة <div> لا تُنشأ إلا إذا كانت السمة URL التي تعرض صفحة تسجيل الدخول تحتوي على المعلمة [error] (http://context/login?error)؛
- السطر 10: السمة [th:if="${param.logout}"] تجعل العلامة <div> لا تُنشأ إلا إذا كانت العلامة URL التي تعرض صفحة تسجيل الدخول تحتوي على المعلمة [logout] (http://context/login?logout)؛
- الأسطر 11-23: نموذج HTML؛
- السطر 11: سيتم إرسال النموذج إلى URL [<context>/login] حيث <context> هو سياق تطبيق الويب؛
- السطر 13: حقل إدخال باسم [username]؛
- السطر 17: حقل إدخال باسم [password]؛
الرمز HTML الذي تم إنشاؤه هو التالي:
يُلاحظ في السطر 21 أن Thymeleaf أضاف حقلًا مخفيًا باسم [_csrf].
2.13.3. تكوين Spring MVC
![]() |
تقوم الفئة [MvcConfig] بتكوين إطار عمل Spring MVC:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
- السطر 7: تعمل التعليقة التوضيحية [@Configuration] على تحويل الفئة [MvcConfig] إلى فئة تكوين؛
- السطر 8: الفئة [MvcConfig] تمتد من الفئة [WebMvcConfigurerAdapter] لإعادة تعريف بعض الطرق فيها؛
- السطر 10: إعادة تعريف إحدى طرق الفئة الأم؛
- الأسطر 11-16: تسمح الطريقة [addViewControllers] بربط URL بعروض HTML. ويتم إجراء الروابط التالية فيها:
عرض | |
/templates/home.html | |
/templates/hello.html | |
/templates/login.html |
اللاحقة [html] والمجلد [templates] هما القيمتان الافتراضيتان اللتان يستخدمهما Thymeleaf. ويمكن تغييرهما من خلال التهيئة. يجب أن يكون المجلد [templates] موجودًا في جذر مسار الفئات (Classpath) للمشروع:
![]() |
في المثال أعلاه [1]، المجلدان [main] و [resources] هما مجلدان مصدران (source folders). وهذا يعني أن محتوياتهما ستكون في جذر مسار الفئات (Classpath) للمشروع. وبالتالي، في المجلد [2]، سيكون المجلدان [hello] و [templates] في جذر مسار الفئات (Classpath).
2.13.4. تكوين Spring Security
![]() |
تقوم الفئة [WebSecurityConfig] بتكوين إطار عمل Spring Security:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
- السطر 9: تعمل العلامة التوضيحية [@Configuration] على تحويل الفئة [WebSecurityConfig] إلى فئة تكوين؛
- السطر 10: التعليق التوضيحي [@EnableWebSecurity] يجعل الفئة [WebSecurityConfig] فئة تكوين لـ Spring Security؛
- السطر 11: الفئة [WebSecurity] تمتد من الفئة [WebSecurityConfigurerAdapter] لإعادة تعريف بعض طرقها؛
- السطر 12: إعادة تعريف إحدى طرق الفئة الأم؛
- الأسطر 13-16: يتم إعادة تعريف الطريقة [configure(HttpSecurity http)] لتحديد حقوق الوصول إلى مختلف URL في التطبيق؛
- السطر 14: تتيح الطريقة [http.authorizeRequests()] ربط URL بحقوق الوصول. ويتم إجراء الروابط التالية فيها:
القاعدة | الرمز | |
الوصول دون مصادقة | | |
الوصول بعد المصادقة فقط |
- السطر 15: يحدد طريقة المصادقة. تتم المصادقة عبر نموذج URL [/login] متاح للجميع [http.formLogin().loginPage("/login").permitAll()]. كما أن تسجيل الخروج (logout) متاح للجميع.
- الأسطر 19-21: تعيد تعريف الطريقة [configure(AuthenticationManagerBuilder auth)] التي تدير المستخدمين؛
- السطر 20: تتم المصادقة باستخدام مستخدمين محددين بشكل «ثابت» [auth.inMemoryAuthentication()]. يتم تعريف المستخدم هنا باستخدام اسم المستخدم [user]، وكلمة المرور [password]، والدور [USER]. يمكن منح نفس الحقوق للمستخدمين الذين لديهم نفس الدور؛
2.13.5. فئة قابلة للتنفيذ
![]() |
الفئة [Application] هي كما يلي:
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {
public static void main(String[] args) throws Throwable {
SpringApplication.run(Application.class, args);
}
}
- السطر 8: يطلب التعليق التوضيحي [@EnableAutoConfiguration] من Spring Boot (السطر 3) إجراء التكوين الذي لم يقم المطور بإجرائه صراحةً؛
- السطر 9: يجعل من الفئة [Application] فئة تكوين Spring؛
- السطر 10: يطلب فحص مجلد الفئة [Application] للبحث عن مكونات Spring. وبالتالي سيتم اكتشاف الفئتين [MvcConfig] و [WebSecurityConfig] لأنهما تحملان التعليق التوضيحي [@Configuration]؛
- السطر 13: الطريقة [main] للفئة القابلة للتنفيذ؛
- السطر 14: يتم تنفيذ الطريقة الثابتة [SpringApplication.run] مع فئة التكوين [Application] كمعلمة. لقد سبق أن تعرفنا على هذه العملية ونعلم أن خادم Tomcat المضمن في تبعيات Maven الخاصة بالمشروع سيتم تشغيله ونشر المشروع عليه. وقد رأينا أن أربع عمليات URL تُدار بواسطة [/, /home, /login, /hello] وأن بعضها محمي بحقوق الوصول.
2.13.6. اختبارات التطبيق
لنبدأ بطلب URL [/]، وهي واحدة من أربع عمليات URL التي تم قبولها. وهي مرتبطة بالعرض [/templates/home.html]:
![]() |
يمكن للجميع الوصول إلى URL المطلوب [/]. ولهذا السبب حصلنا عليه. الرابط [here] هو التالي:
سيتم طلب URL [/hello] عند النقر على الرابط. وهذا الرابط محمي:
القاعدة | الرمز | |
الدخول دون مصادقة | | |
الوصول بعد المصادقة فقط |
يجب المصادقة للحصول عليه. سيقوم Spring Security عندئذ بإعادة توجيه متصفح العميل إلى صفحة المصادقة. وفقًا للتكوين المعروض، فإنها صفحة URL [/login]. هذه الصفحة متاحة للجميع:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
وبالتالي نحصل على [1]:
![]() |
الرمز المصدري للصفحة التي تم الحصول عليها هو التالي:
- في السطر 7، يظهر حقل مخفي غير موجود في الصفحة الأصلية [login.html]. وقد أضافه Thymeleaf. ويهدف هذا الرمز المسمى CSRF (تزوير الطلبات عبر المواقع) إلى إزالة ثغرة أمنية. يجب إعادة إرسال هذا الرمز إلى Spring Security مع المصادقة حتى يتم قبولها؛
ونذكر أن Spring Security لا يعترف إلا بالمستخدم user/password. إذا أدخلنا أي شيء آخر في [2]، فسنحصل على نفس الصفحة مع رسالة خطأ في [3]. قام Spring Security بإعادة توجيه المتصفح إلى URL [http://localhost:8080/login?error]. أدى وجود المعلمة [error] إلى عرض العلامة:
<div th:if="${param.error}">Invalid username and password.</div>
الآن، دعونا ندخل القيم المتوقعة لـ user/password [4]:
![]() |
- في [4]، نقوم بتسجيل الدخول؛
- في [5]، يقوم Spring Security بإعادة توجيهنا إلى URL [/hello] لأن URL هو ما كنا نطلبه عندما تمت إعادة توجيهنا إلى صفحة تسجيل الدخول. تم عرض هوية المستخدم في السطر التالي من [hello.html]:
تعرض الصفحة [5] النموذج التالي:
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
عند النقر على الزر [Sign Out]، سيتم إجراء عملية POST على URL و[/logout]. وهذا الملف، مثل ملف URL و[/login]، متاح للجميع:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
في جمعيتنا URL / vues، لم نحدد أي شيء بخصوص URL و [/logout]. ماذا سيحدث؟ لنجرب:
![]() |
- في [6]، نضغط على الزر [Sign Out]؛
- في [7]، نلاحظ أننا تمت إعادة توجيهنا إلى URL [http://localhost:8080/login?logout]. Spring Security هو الذي طلب إعادة التوجيه هذه. أدى وجود المعلمة [logout] في URL إلى عرض السطر التالي في العرض:
<div th:if="${param.logout}">You have been logged out.</div>
2.13.7. الخلاصة
في المثال السابق، كان بإمكاننا كتابة تطبيق الويب أولاً ثم تأمينه لاحقًا. Spring Security ليس تدخليًا. يمكننا تطبيق الأمان على تطبيق ويب مكتوب بالفعل. علاوة على ذلك، اكتشفنا النقاط التالية:
- من الممكن تعريف صفحة مصادقة؛
- يجب أن تكون المصادقة مصحوبة برمز CSRF الصادر عن Spring Security؛
- إذا فشلت عملية المصادقة، يتم إعادة توجيه المستخدم إلى صفحة المصادقة مع إضافة معلمة error في الرمز URL؛
- إذا نجحت عملية المصادقة، يتم إعادة توجيه المستخدم إلى الصفحة المطلوبة عند إتمام المصادقة. إذا تم طلب صفحة المصادقة مباشرةً دون المرور بصفحة وسيطة، فإن Spring Security يعيد توجيهنا إلى URL [/] (لم يتم عرض هذه الحالة)؛
- يتم تسجيل الخروج عن طريق طلب الصفحة URL [/logout] باستخدام معلمة POST. ثم يقوم Spring Security بإعادة توجيهنا إلى صفحة المصادقة مع المعلمة logout في URL؛
تستند جميع هذه الاستنتاجات إلى السلوكيات الافتراضية لـ Spring Security. ويمكن تغيير هذه السلوكيات عن طريق التهيئة من خلال إعادة تعريف بعض الطرق في الفئة [WebSecurityConfigurerAdapter].
لن يفيدنا البرنامج التعليمي السابق كثيرًا في الخطوات التالية. سنستخدم بالفعل:
- قاعدة بيانات لتخزين المستخدمين وكلمات مرورهم وأدوارهم؛
- المصادقة عبر الرأس HTTP؛
لا توجد سوى القليل من الدروس التعليمية لما نريد القيام به هنا. الحل الذي سيتم اقتراحه هو تجميع لأكواد تم العثور عليها هنا وهناك.
2.14. تفعيل الأمان على خدمة الويب الخاصة بالمواعيد
2.14.1. قاعدة البيانات
تتطور قاعدة البيانات [rdvmedecins] لتشمل المستخدمين وكلمات مرورهم وأدوارهم. تظهر ثلاث جداول جديدة:

الجدول [USERS]: المستخدمون
- ID: المفتاح الأساسي؛
- VERSION: عمود إصدار السطر؛
- IDENTITY: هوية وصفية للمستخدم؛
- LOGIN: اسم تسجيل دخول المستخدم؛
- PASSWORD: كلمة المرور الخاصة به؛
في الجدول USERS، لا يتم تخزين كلمات المرور بشكل عادي:
![]() |
الخوارزمية التي تقوم بتشفير كلمات المرور هي الخوارزمية BCRYPT.
الجدول [ROLES]: الأدوار
- ID: المفتاح الأساسي؛
- VERSION: عمود إصدار السطر؛
- NAME: اسم الدور. بشكل افتراضي، يتوقع Spring Security أسماءً بالصيغة ROLE_XX، على سبيل المثال ROLE_ADMIN أو ROLE_GUEST؛
![]() |
الجدول [USERS_ROLES]: جدول الربط بين USERS و ROLES
يمكن أن يكون للمستخدم عدة أدوار، ويمكن أن يضم الدور عدة مستخدمين. لدينا علاقة متعددة إلى متعددة تتجسد في الجدول [USERS_ROLES].
- ID: المفتاح الأساسي؛
- VERSION: عمود إصدار السطر؛
- USER_ID: معرّف المستخدم؛
- ROLE_ID: معرّف الدور؛
![]() |
نظرًا لأننا نقوم بتعديل قاعدة البيانات، يجب تعديل جميع طبقات المشروع [métier, DAO, JPA]:
![]() |
2.14.2. مشروع Eclipse الجديد [métier, DAO, JPA]
نقوم بنسخ المشروع الأصلي [rdvmedecins-metier-dao] إلى [rdvmedecins-metier-dao-v2]:
![]() |
- إلى [1]: المشروع الجديد؛
- إلى [2]: تم تجميع التعديلات التي تم إجراؤها مراعاةً للأمن في حزمة واحدة هي [rdvmedecins.security]. تنتمي هذه العناصر الجديدة إلى الطبقات [JPA] و [DAO]، لكن لتبسيط الأمور قمت بتجميعها في حزمة واحدة.
2.14.3. الكيانات الجديدة [JPA]
![]() |
تحدد الطبقة JPA ثلاث كيانات جديدة:
![]() |
الفئة [User] هي صورة للجدول [USERS]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {
private static final long serialVersionUID = 1L;
// خصائص
private String identity;
private String login;
private String password;
// الشركة المصنعة
public User() {
}
public User(String identity, String login, String password) {
this.identity = identity;
this.login = login;
this.password = password;
}
// الهوية
@Override
public String toString() {
return String.format("User[%s,%s,%s]", identity, login, password);
}
// أدوات الاسترجاع والتعيين
....
}
- السطر 9: الفئة تمتد من الفئة [AbstractEntity] التي سبق استخدامها للكيانات الأخرى؛
- الأسطر 13-15: لم يتم تحديد أسماء للأعمدة لأنها تحمل نفس أسماء الحقول المرتبطة بها؛
الفئة [Role] هي نسخة طبق الأصل من الجدول [ROLES]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {
private static final long serialVersionUID = 1L;
// الخصائص
private String name;
// الشركات المصنعة
public Role() {
}
public Role(String name) {
this.name = name;
}
// الهوية
@Override
public String toString() {
return String.format("Role[%s]", name);
}
// الوصول إلى القيم والتعيين
...
}
الفئة [UserRole] هي صورة الجدول [USERS_ROLES]:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {
private static final long serialVersionUID = 1L;
// يشير UserRole إلى مستخدم
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
// تشير UserRole إلى دور
@ManyToOne
@JoinColumn(name = "ROLE_ID")
private Role role;
// وظائف الحصول والتعيين
...
}
- الأسطر 15-17: تمثل المفتاح الأجنبي من الجدول [USERS_ROLES] إلى الجدول [USERS]؛
- الأسطر 19-21: تُجسد المفتاح الأجنبي للجدول [USERS_ROLES] إلى الجدول [ROLES]؛
2.14.4. التعديلات على الطبقة [DAO]
![]() |
تمت إضافة ثلاثة عناصر جديدة إلى الطبقة [DAO]، وهي:
![]() |
تدير الواجهة [UserRepository] الوصول إلى الكيانات [User]:
package rdvmedecins.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Role;
import rdvmedecins.entities.User;
public interface UserRepository extends CrudRepository<User, Long> {
// قائمة الأدوار الخاصة بمستخدم محدد بواسطة معرّفه
@Query("select ur.role from UserRole ur where ur.user.id=?1")
Iterable<Role> getRoles(long id);
// قائمة الأدوار الخاصة بمستخدم تم تحديده بواسطة اسم تسجيل الدخول وكلمة المرور
@Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
Iterable<Role> getRoles(String login, String password);
// البحث عن مستخدم باستخدام اسم تسجيل الدخول الخاص به
User findUserByLogin(String login);
}
- السطر 9: واجهة [UserRepository] توسع واجهة [CrudRepository] الخاصة بـ Spring Data (السطر 4)؛
- السطران 12-13: تتيح الطريقة [getRoles(User user)] الحصول على جميع أدوار المستخدم الذي تم تحديد هويته بواسطة [id]
- السطران 16-17: نفس الشيء ولكن بالنسبة لمستخدم يتم تعريفه بواسطة اسم المستخدم وكلمة المرور؛
تدير الواجهة [RoleRepository] الوصول إلى الكيانات [Role]:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface RoleRepository extends CrudRepository<Role, Long> {
// البحث عن دور باستخدام اسمه
Role findRoleByName(String name);
}
- السطر 5: الواجهة [RoleRepository] توسع نطاق الواجهة [CrudRepository]؛
- السطر 8: يمكن البحث عن دور باستخدام اسمه؛
تدير الواجهة [userRoleRepository] الوصول إلى الكيانات [UserRole]:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
}
- السطر 5: تكتفي الواجهة [UserRoleRepository] بتوسيع الواجهة [CrudRepository] دون إضافة أساليب جديدة إليها؛
2.14.5. فئات إدارة المستخدمين والأدوار
![]() |
يفرض Spring Security إنشاء فئة تنفذ الواجهة التالية [UsersDetail]:
![]() |
يتم تنفيذ هذه الواجهة هنا بواسطة الفئة [AppUserDetails]:
package rdvmedecins.security;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class AppUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
// الخصائص
private User user;
private UserRepository userRepository;
// المنشئون
public AppUserDetails() {
}
public AppUserDetails(User user, UserRepository userRepository) {
this.user = user;
this.userRepository = userRepository;
}
// -------------------------واجهة
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : userRepository.getRoles(user.getId())) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getLogin();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
// أدوات الاسترجاع والتعيين
...
}
- السطر 10: الفئة [AppUserDetails] تُنفذ الواجهة [UserDetails]؛
- السطران 15-16: تُغلف الفئة مستخدمًا (السطر 15) والمستودع الذي يتيح الحصول على تفاصيل هذا المستخدم (السطر 16)؛
- الأسطر 22-25: المنشئ الذي يقوم بإنشاء مثيل للفئة باستخدام مستخدم ومستودعه؛
- الأسطر 28-35: تنفيذ الطريقة [getAuthorities] للواجهة [UserDetails]. يجب أن تقوم بإنشاء مجموعة من العناصر من النوع [GrantedAuthority] أو أحد مشتقاته. هنا، نستخدم النوع المشتق [SimpleGrantedAuthority] (السطر 32) الذي يغلف اسم أحد أدوار المستخدم المذكور في السطر 15؛
- الأسطر 31-33: يتم تصفح قائمة أدوار المستخدم في السطر 15 لإنشاء قائمة بالعناصر من النوع [SimpleGrantedAuthority]؛
- الأسطر 38-40: تُنفذ الطريقة [getPassword] الخاصة بالواجهة [UserDetails]. يتم إرجاع كلمة مرور المستخدم المذكور في السطر 15؛
- الأسطر 38-40: تُنفذ الطريقة [getUserName] الخاصة بالواجهة [UserDetails]. يتم إرجاع اسم تسجيل الدخول للمستخدم المذكور في السطر 15؛
- الأسطر 47-50: لا تنتهي صلاحية حساب المستخدم أبدًا؛
- الأسطر 52-55: لا يتم حظر حساب المستخدم أبدًا؛
- الأسطر 57-60: لا تنتهي صلاحية بيانات اعتماد المستخدم أبدًا؛
- الأسطر 62-65: يظل حساب المستخدم نشطًا دائمًا؛
يفرض Spring Security أيضًا وجود فئة تُنفِّذ الواجهة [AppUserDetailsService]:
![]() |
يتم تنفيذ هذه الواجهة بواسطة الفئة [AppUserDetails] التالية:
package rdvmedecins.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class AppUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
// البحث عن المستخدم عبر اسم تسجيل الدخول
User user = userRepository.findUserByLogin(login);
// هل تم العثور عليه؟
if (user == null) {
throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
}
// عرض تفاصيل المستخدم
return new AppUserDetails(user, userRepository);
}
}
- السطر 9: ستكون الفئة مكونًا من مكونات Spring، وبالتالي ستكون متاحة في سياقها؛
- السطران 12-13: سيتم حقن المكون [UserRepository] هنا؛
- الأسطر 16-25: تنفيذ الطريقة [loadUserByUsername] الخاصة بالواجهة [UserDetailsService] (السطر 10). المعلمة هي اسم تسجيل دخول المستخدم؛
- السطر 18: يتم البحث عن المستخدم باستخدام اسم تسجيل الدخول الخاص به؛
- الأسطر 20-22: إذا لم يتم العثور عليه، يتم إلقاء استثناء؛
- السطر 24: يتم إنشاء كائن [AppUserDetails] وعرضه. وهو بالفعل من النوع [UserDetails] (السطر 16)؛
2.14.6. اختبارات الطبقة [DAO]
![]() |
أولاً، نقوم بإنشاء فئة قابلة للتنفيذ [CreateUser] قادرة على إنشاء مستخدم مع دور:
package rdvmedecins.security;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.security.Role;
import rdvmedecins.security.RoleRepository;
import rdvmedecins.security.User;
import rdvmedecins.security.UserRepository;
import rdvmedecins.security.UserRole;
import rdvmedecins.security.UserRoleRepository;
public class CreateUser {
public static void main(String[] args) {
// الصيغة: اسم المستخدم وكلمة المرور roleName
// يلزم وجود ثلاثة معلمات
if (args.length != 3) {
System.out.println("Syntaxe : [pg] user password role");
System.exit(0);
}
// يتم استرداد المعلمات
String login = args[0];
String password = args[1];
String roleName = String.format("ROLE_%s", args[2].toUpperCase());
// سياق Spring
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
UserRepository userRepository = context.getBean(UserRepository.class);
RoleRepository roleRepository = context.getBean(RoleRepository.class);
UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
// هل الدور موجود بالفعل؟
Role role = roleRepository.findRoleByName(roleName);
// إذا لم يكن موجودًا، يتم إنشاؤه
if (role == null) {
role = roleRepository.save(new Role(roleName));
}
// هل المستخدم موجود بالفعل؟
User user = userRepository.findUserByLogin(login);
// إذا لم يكن موجودًا، فسنقوم بإنشائه
if (user == null) {
// نقوم بتشفير كلمة المرور باستخدام bcrypt
String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
// نقوم بحفظ المستخدم
user = userRepository.save(new User(login, login, crypt));
// يتم إنشاء الارتباط بالدور
userRoleRepository.save(new UserRole(user, role));
} else {
// المستخدم موجود بالفعل - هل لديه الدور المطلوب؟
boolean trouvé = false;
for (Role r : userRepository.getRoles(user.getId())) {
if (r.getName().equals(roleName)) {
trouvé = true;
break;
}
}
// إذا لم يتم العثور عليه، يتم إنشاء العلاقة مع الدور
if (!trouvé) {
userRoleRepository.save(new UserRole(user, role));
}
}
// إغلاق سياق Spring
context.close();
}
}
- السطر 17: تتوقع الفئة ثلاث معلمات تحدد المستخدم: اسم تسجيل الدخول، وكلمة المرور، والدور؛
- الأسطر 25-27: يتم استرداد المعلمات الثلاثة؛
- السطر 29: يتم إنشاء سياق Spring استنادًا إلى فئة التكوين [DomainAndPersistenceConfig]. كانت هذه الفئة موجودة بالفعل في المشروع السابق. ويجب تعديلها على النحو التالي:
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities", "rdvmedecins.security" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
....
}
- السطر 1: يجب الإشارة إلى وجود مكونات [Repository] الآن في الحزمة [rdvmedecins.security]؛
- السطر 4: يجب الإشارة إلى أن هناك الآن كيانات JPA في الحزمة [rdvmedecins.security]؛
لنعد إلى كود إنشاء المستخدم:
- الأسطر 30-32: نسترد مراجع الكيانات الثلاثة [Repository] التي قد تكون مفيدة لنا لإنشاء المستخدم؛
- السطر 34: نتحقق مما إذا كان الدور موجودًا بالفعل؛
- الأسطر 36-38: إذا لم يكن موجودًا، نقوم بإنشائه في قاعدة البيانات. وسيكون اسمه من النوع [ROLE_XX]؛
- السطر 40: نتحقق مما إذا كان اسم تسجيل الدخول موجودًا بالفعل؛
- الأسطر 42-49: إذا لم يكن اسم المستخدم موجودًا، يتم إنشاؤه في قاعدة البيانات؛
- السطر 44: يتم تشفير كلمة المرور. نستخدم هنا فئة [BCrypt] من Spring Security (السطر 4). لذا نحتاج إلى أرشيفات هذا الإطار. يتضمن الملف [pom.xml] تبعية جديدة:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- السطر 46: يتم حفظ بيانات المستخدم في قاعدة البيانات؛
- السطر 48: وكذلك العلاقة التي تربطه بدوره؛
- الأسطر 51-57: في حالة وجود تسجيل الدخول بالفعل – يتم عندئذٍ التحقق مما إذا كان الدور الذي نريد تعيينه له موجودًا بالفعل ضمن أدواره؛
- السطور 59-61: إذا لم يتم العثور على الدور المطلوب، يتم إنشاء سطر في الجدول [USERS_ROLES] لربط المستخدم بدوره؛
- لم يتم اتخاذ تدابير للحماية من الاستثناءات المحتملة. هذه فئة مساعدة لإنشاء مستخدم مع دور بسرعة.
عند تشغيل الفئة باستخدام الوسيطات [x x guest]، نحصل في قاعدة البيانات على النتائج التالية:
الجدول [USERS]
![]() |
الجدول [ROLES]
![]() |
الجدول [USERS_ROLES]
![]() |
لننظر الآن إلى الفئة الثانية [UsersTest] التي تمثل اختبارًا لـ JUnit:
![]() |
package rdvmedecins.security;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import rdvmedecins.config.DomainAndPersistenceConfig;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {
@Autowired
private UserRepository userRepository;
@Autowired
private AppUserDetailsService appUserDetailsService;
@Test
public void findAllUsersWithTheirRoles() {
Iterable<User> users = userRepository.findAll();
for (User user : users) {
System.out.println(user);
display("Roles :", userRepository.getRoles(user.getId()));
}
}
@Test
public void findUserByLogin() {
// يتم استرداد المستخدم [admin]
User user = userRepository.findUserByLogin("admin");
// نتحقق من أن كلمة مروره هي [admin]
Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
// التحقق من دور admin / admin
List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
Assert.assertEquals(1L, roles.size());
Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
}
@Test
public void loadUserByUsername() {
// يتم استرداد المستخدم [admin]
AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
// نتحقق من أن كلمة المرور الخاصة به هي [admin]
Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
// يتم التحقق من دور admin / admin
@SuppressWarnings("unchecked")
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
Assert.assertEquals(1L, authorities.size());
Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
}
// أداة مساعدة - تعرض عناصر مجموعة ما
private void display(String message, Iterable<?> elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- الأسطر 27-34: اختبار بصري. يتم عرض جميع المستخدمين مع أدوارهم؛
- الأسطر 36-46: نتحقق من أن المستخدم [admin] لديه كلمة المرور [admin] والدور [ROLE_ADMIN] باستخدام المستودع [UserRepository]؛
- السطر 41: [admin] هي كلمة المرور غير المشفرة. في قاعدة البيانات، يتم تشفيرها وفقًا لخوارزمية BCrypt. تسمح الطريقة [ BCrypt.checkpw] بالتحقق من أن كلمة المرور غير المشفرة، بعد تشفيرها، تتطابق بالفعل مع تلك الموجودة في قاعدة البيانات؛
- الأسطر 48-59: يتم التحقق من أن المستخدم [admin] لديه كلمة المرور [admin] والدور [ROLE_ADMIN] باستخدام الخدمة [appUserDetailsService]؛
تم تنفيذ الاختبارات بنجاح مع السجلات التالية:
2.14.7. استنتاج مؤقت
تمت إضافة الفئات اللازمة لـ Spring Security مع إجراء تعديلات طفيفة على المشروع الأصلي. وللتذكير، كانت هذه التعديلات كما يلي:
- إضافة تبعية لـ Spring Security في الملف [pom.xml]؛
- إنشاء ثلاث جداول إضافية في قاعدة البيانات؛
- إنشاء كيانات JPA ومكونات Spring في الحزمة [rdvmedecins.security]؛
ينبع هذا السيناريو المواتي للغاية من حقيقة أن الجداول الثلاثة المضافة إلى قاعدة البيانات مستقلة عن الجداول الموجودة. بل كان من الممكن وضعها في قاعدة بيانات منفصلة. وقد كان ذلك ممكنًا لأننا قررنا أن المستخدم له وجود مستقل عن الأطباء والعملاء. ولو كان هؤلاء الأطباء والعملاء مستخدمين محتملين، لكان من الضروري إنشاء روابط بين الجدول [USERS] والجدولين [MEDECINS] و[CLIENTS]. وكان ذلك سيؤثر بشكل كبير على المشروع الحالي.
2.14.8. مشروع Eclipse الخاص بالطبقة [web]
![]() |
تم تكرار المشروع [rdvmedecins-webapi] السابق في المشروع [rdvmedecins-webapi-v2] [1]:
![]() |
التعديلات الوحيدة التي يجب إجراؤها هي في الحزمة [rdvmedecins.web.config] حيث يجب تكوين Spring Security. وقد صادفنا بالفعل فئة تكوين لـ Spring Security:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
وسنتبع نفس الخطوات:
- السطر 11: تعريف فئة تمتد من الفئة [WebSecurityConfigurerAdapter]؛
- السطر 13: تعريف طريقة [configure(HttpSecurity http)] التي تحدد حقوق الوصول إلى مختلف فئات URL في خدمة الويب؛
- السطر 19: تعريف طريقة [configure(AuthenticationManagerBuilder auth)] التي تحدد المستخدمين وأدوارهم؛
يتم تكوين Spring Security بواسطة الفئة [SecurityConfig]:
package rdvmedecins.web.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import rdvmedecins.security.AppUserDetailsService;
@EnableAutoConfiguration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AppUserDetailsService appUserDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder registry) throws Exception {
// تتم المصادقة بواسطة الكائن [appUserDetailsService]
// يتم تشفير كلمة المرور باستخدام خوارزمية التجزئة Bcrypt
registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF
http.csrf().disable();
// يتم إرسال كلمة المرور عبر رأس الرسالة Authorization: Basic xxxx
http.httpBasic();
// لا يمكن استخدام التطبيق إلا من قبل الدور ADMIN
http.authorizeRequests() //
.antMatchers("/", "/**") // جميع URL
.hasRole("ADMIN");
}
}
- السطران 14-15: تم استخدام التعليقات التوضيحية الواردة في المثال؛
- السطران 17-18: يتم إدخال الفئة [AppUserDetails] التي تتيح الوصول لمستخدمي التطبيق؛
- السطران 20-21: تحدد الطريقة [configure(HttpSecurity http)] المستخدمين وأدوارهم. وتتلقى كمعلمة نوعًا [AuthenticationManagerBuilder]. ويتم إثراء هذه المعلمة بمعلومتين:
- مرجع إلى الخدمة [appUserDetailsService] الواردة في السطر 18 والتي تمنح حق الوصول للمستخدمين المسجلين. وتجدر الإشارة هنا إلى أن حقيقة تسجيلهم في قاعدة بيانات لا تظهر. لذا، قد يكونون موجودين في ذاكرة التخزين المؤقت، أو يتم توفيرهم بواسطة خدمة ويب، ...
- نوع التشفير المستخدم لكلمة المرور. ونذكر هنا أننا استخدمنا الخوارزمية BCrypt؛
- الأسطر 27-40: تحدد الطريقة [configure(HttpSecurity http)] حقوق الوصول إلى URL الخاصة بخدمة الويب؛
- السطر 30: رأينا في المشروع التمهيدي أن Spring Security يدير افتراضيًا رمزًا CSRF (تزوير الطلبات عبر المواقع) يجب على المستخدم الذي يرغب في المصادقة إرساله إلى الخادم. هنا، تم تعطيل هذه الآلية؛
- السطر 32: يتم تفعيل وضع المصادقة عبر الرأس HTTP. سيتعين على العميل إرسال الرأس HTTP التالي:
حيث «code» هو ترميز السلسلة «login:password» باستخدام خوارزمية Base64. على سبيل المثال، ترميز Base64 للسلسلة admin:admin هو YWRtaW46YWRtaW4=. وبالتالي، فإن المستخدم الذي يستخدم اسم المستخدم [admin] وكلمة المرور [admin] سيرسل الرأس التالي HTTP لتوثيق هويته:
- الأسطر 34-36: تشير إلى أن جميع URL الخاصة بخدمة الويب متاحة للمستخدمين الذين لديهم الدور [ROLE_ADMIN]. وهذا يعني أن المستخدم الذي لا يمتلك هذا الدور لا يمكنه الوصول إلى خدمة الويب؛
تتطور الفئة [AppConfig] التي تهيئ التطبيق بأكمله على النحو التالي:
![]() |
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class })
public class AppConfig {
}
- يتم إجراء التعديل في السطر 11: يُشار إلى أن هناك الآن ملفين للتكوين يجب استخدامهما هما [DomainAndPersistenceConfig] و [SecurityConfig].
2.14.9. اختبارات خدمة الويب
سنقوم باختبار خدمة الويب باستخدام متصفح Chrome [Advanced Rest Client]. سنحتاج إلى تحديد رأس المصادقة HTTP:
حيث [code] هو رمز Base64 للسلسلة [login:password]. لتوليد هذا الرمز، يمكن استخدام البرنامج التالي:
![]() |
package rdvmedecins.helpers;
import org.springframework.security.crypto.codec.Base64;
public class Base64Encoder {
public static void main(String[] args) {
// يُتوقع وجود معلمتين: اسم المستخدم وكلمة المرور
if (args.length != 2) {
System.out.println("Syntaxe : login password");
System.exit(0);
}
// يتم استرداد المعلمتين
String chaîne = String.format("%s:%s", args[0], args[1]);
// يتم ترميز السلسلة
byte[] data = Base64.encode(chaîne.getBytes());
// يتم عرض الترميز Base64 الخاص بها
System.out.println(new String(data));
}
}
إذا قمنا بتشغيل هذا البرنامج مع المعلمتين [admin admin]:
![]() |
نحصل على النتيجة التالية:
والآن بعد أن أصبحنا قادرين على إنشاء رأس المصادقة HTTP، نقوم بتشغيل خدمة الويب الآمنة. ثم باستخدام متصفح Chrome [Advanced Rest Client]، نطلب قائمة بجميع الأطباء:
![]() |
- في [1]، نطلب URL الخاص بالأطباء؛
- في [2]، باستخدام طريقة GET؛
- في [3]، نقدم رأس المصادقة HTTP. الرمز [YWRtaW46YWRtaW4=] هو ترميز Base64 للسلسلة [admin:admin]؛
- في [4]، نرسل الأمر HTTP؛
رد الخادم هو كما يلي:
![]() |
- في [1]، رأس المصادقة HTTP؛
- في [2]، يرد الخادم برد JSON؛
- في [3]، قائمة الأطباء.
لنحاول الآن إرسال طلب HTTP برأس مصادقة غير صحيح. تكون الاستجابة عندئذٍ كما يلي:
![]() |
- في [1] و [3]: رأس المصادقة HTTP؛
- إلى [2]: استجابة خدمة الويب؛
الآن، لنجرب المستخدم user / user. إنه موجود ولكنه لا يملك حق الوصول إلى خدمة الويب. إذا قمنا بتشغيل برنامج الترميز Base64 مع الحجتين [user user]:
![]() |
نحصل على النتيجة التالية:
![]() |
- في [1] و [3]: رأس المصادقة HTTP؛
- إلى [2]: استجابة خدمة الويب. وهي تختلف عن الاستجابة السابقة التي كانت [401 Unauthorized]. هذه المرة، قام المستخدم بالمصادقة بشكل صحيح ولكنه لا يمتلك الصلاحيات الكافية للوصول إلى URL؛
2.15. Conclusion
لنتذكر البنية العامة لتطبيقنا العميل/الخادم:
![]() |
أصبحت خدمة الويب الآمنة جاهزة للعمل الآن. سنرى أنه سيتعين تعديلها نتيجة لمشاكل ستظهر عند إنشاء عميل Angular JS. لكننا سننتظر حتى نواجه المشكلة لحلها. سنقوم الآن بإنشاء عميل Angular الذي سيوفر واجهة ويب لإدارة مواعيد الأطباء.

















































































































































