8. مطالعه موردی
8.1. Introduction
ما پیشنهاد میکنیم یک اپلیکیشن وب برای رزرو وقت ملاقات در یک مطب پزشکی بنویسیم. این مسئله در سند «AngularJS / Spring 4 Tutorial» در URL [http://tahe.developpez.com/angularjs-spring4/] پوشش داده شده است. معماری این اپلیکیشن به شرح زیر بود:
![]() |
- در [1]، یک سرور وب صفحات ایستا را به یک مرورگر تحویل میدهد. این صفحات حاوی یک برنامه AngularJS هستند که بر روی مدل MVC (مدل–نما–کنترلکننده) ساخته شده است. مدل در اینجا شامل هر دو نماها و دامنه است که با لایه [Services] نشان داده شده است؛
- کاربر با نماهای ارائهشده در مرورگر تعامل خواهد کرد. اقدامات آنها گاهی اوقات مستلزم ارسال درخواست به سرور Spring 4 [2] خواهد بود. سرور درخواست را پردازش کرده و یک پاسخ jSON (JavaScript نشانهگذاری شیء) [3] را بازمیگرداند. این پاسخ برای بهروزرسانی نمای ارائهشده به کاربر استفاده خواهد شد.
ما پیشنهاد میکنیم این برنامه را بهصورت کامل و از ابتدا تا انتها با استفاده از Spring MVC پیادهسازی کنیم. معماری آن به شکل زیر درمیآید:
![]() |
مرورگر به یک برنامه [Web 1] که با استفاده از Spring MVC پیادهسازی شده است متصل میشود، که دادههای خود را از یک سرویس وب [Web 2]، که آن نیز با استفاده از Spring MVC پیادهسازی شده است، بازیابی میکند.
8.2. ویژگیهای برنامه
از خوانندگان دعوت میشود تا با آزمایش این برنامه، ویژگیهای آن را کشف کنند. ما پروژههای Maven را از پوشه [etude-de-cas] به STS بارگذاری میکنیم:
![]() | ![]() |
ابتدا، ما با استفاده از ابزار [Wamp Server] پایگاه داده MySQL 5 [dbrdvmedecins] را ایجاد خواهیم کرد (به بخش 9.5 مراجعه کنید):
![]() |
- در [1]، ابزار [phpMyAdmin] را از WampServer انتخاب کنید؛
- در [2]، گزینه [Importer] را انتخاب کنید؛
![]() |
- در [3]، فایل [database/dbrdvmedecins.sql] را انتخاب کنید؛
- برای [4]، آن را اجرا کنید؛
- در [5]، پایگاه داده ایجاد میشود.
سپس باید سرور متصل به پایگاه داده را راهاندازی کنیم. این پروژه [rdvmedecins-webjson-server] است.
![]() |
سرور در URL [http://localhost:8080] در دسترس خواهد بود. این را میتوان در فایل [application.properties] پروژه تغییر داد:
![]() |
server.port=8080
تنظیمات دسترسی به پایگاه داده در کلاس [DomainAndPersistenceConfig] پروژه [rdvmedecins-metier-dao] ذخیره میشوند:
![]() |
//منبع داده 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;
}
اگر با اعتبارنامههای متفاوت به SGBD یا MySQL دسترسی پیدا کنید، اینجاست که اتفاق میافتد.
سپس سرور [rdvmedecins-springthymeleaf-server] را به همان شیوهٔ سرور قبلی راهاندازی میکنیم:
![]() | ![]() |
به طور پیشفرض، این سرور در URL [http://localhost:8081] در دسترس است. مجدداً، این مورد را میتوان در فایل [application.properties] پروژه پیکربندی کرد:
server.port=8081
علاوه بر این، این سرور باید URL سرور متصل به پایگاه داده را بداند. این پیکربندی را میتوان در کلاس [AppConfig] بالا یافت:
// مدیر / مدیر
private final String USER_INIT = "admin";
private final String MDP_USER_INIT = "admin";
//سرویس وب ریشه / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// زمانانتظار به میلیثانیه
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
اگر سرور اول روی پورتی غیر از 8080 راهاندازی شده باشد، خط 5 باید اصلاح شود.
سپس، با استفاده از یک مرورگر وب، URL و [http://localhost:8081/boot.html] را درخواست کنید:
![]() |
- به [1]، صفحه ورود برنامه؛
- به [1]، نام کاربری و رمز عبور فردی که مایل به استفاده از برنامه است. دو کاربر وجود دارد: admin/admin (نام کاربری/رمز عبور) با نقش (ADMIN) و user/user با نقش (USER). تنها نقش ADMIN مجاز به استفاده از برنامه است. نقش USER صرفاً برای نمایش پاسخ سرور در این مورد استفاده گنجانده شده است؛
- در [4]، دکمهای که برای ورود به سرور استفاده میشود؛
- در [5]، زبان برنامه. دو گزینه وجود دارد: فرانسوی (پیشفرض) و انگلیسی؛
- در [6]، URL از سرور [rdvmedecins-springthymeleaf-server]؛
![]() |
- در [1]، شما وارد میشوید؛
![]() |
- پس از ورود به سیستم، میتوانید پزشکی را که مایل به رزرو وقت ملاقات با او هستید ([2]) و تاریخ قرار ملاقات ([3]) را انتخاب کنید. به محض انتخاب پزشک و تاریخ، تقویم به طور خودکار نمایش داده میشود:
![]() |
- پس از دسترسی به تقویم پزشک، میتوانید یک نوبت رزرو کنید [5]؛
![]() |
- در [6]، بیمار را برای قرار ملاقات انتخاب کرده و این انتخاب را در [7] تأیید میکنید؛
![]() |
پس از تأیید قرار ملاقات، شما به طور خودکار به تقویم بازمیگردید که در آن قرار ملاقات جدید اکنون فهرست شده است. این قرار ملاقات را میتوان در تاریخ بعدی با استفاده از [8] حذف کرد.
ویژگیهای اصلی توضیح داده شدند. آنها ساده هستند. بیایید با مدیریت زبان به پایان برسیم:
1

- در [1]، شما از فرانسوی به انگلیسی تغییر میدهید؛
![]() |
- در [2]، نما به انگلیسی تغییر کرده است، از جمله تقویم؛
8.3. پایگاه داده
![]() |
پایگاه داده، که از این پس با نام [dbrdvmedecins] ذکر میشود، یک پایگاه داده MySQL5 است که شامل جداول زیر میباشد:
![]() |
قرارها توسط جداول زیر مدیریت میشوند:
- [medecins]: شامل فهرست پزشکان مطب است؛
- [clients]: شامل فهرست بیماران مطب است؛
- [creneaux]: شامل اسلاتهای قرار ملاقات برای هر پزشک است؛
- [rv]: شامل فهرست قرارهای ملاقات پزشکان است.
جدولهای [roles]، [users] و [users_roles] مربوط به احراز هویت هستند. در حال حاضر با آنها سروکار نخواهیم داشت. روابط بین جدولهای مدیریت قرار ملاقاتها به شرح زیر است:
![]() |
- یک بازه زمانی متعلق به یک پزشک است – یک پزشک ۰ یا چند بازه زمانی دارد؛
- یک قرار ملاقات، مراجع و پزشک را از طریق یکی از بازههای زمانی پزشک گرد هم میآورد؛
- یک مراجعهکننده ۰ یا چند قرار ملاقات دارد؛
- یک بازه زمانی با ۰ یا چند قرار ملاقات (در روزهای مختلف) مرتبط است.
8.3.1. جدول [MEDECINS]
این شامل اطلاعاتی دربارهٔ پزشکان مدیریتشده توسط برنامهٔ [RdvMedecins] است.
![]() | ![]() |
- ID: شماره شناسایی پزشک – کلید اصلی جدول
- VERSION: شمارهای که نسخهٔ سطر در جدول را شناسایی میکند. این شماره هر بار که تغییری در سطر ایجاد میشود، یک واحد افزایش مییابد.
- NOM: نام خانوادگی پزشک
- PRENOM: نام کوچک آنها
- TITRE: عنوان آنها (خانم، بانو، آقا)
8.3.2. جدول [CLIENTS]
بیماران پزشکان مختلف در جدول [CLIENTS] ثبت میشوند:
![]() | ![]() |
- ID: شماره شناسه مشتری – کلید اصلی جدول
- VERSION: شمارهای که نسخهٔ سطر در جدول را شناسایی میکند. این شماره هر بار که تغییری در سطر ایجاد شود، یک واحد افزایش مییابد.
- NOM: نام خانوادگی مشتری
- PRENOM: نام کوچک آنها
- TITRE: عنوان آنها (خانم، بانو، آقای)
8.3.3. جدول [CRENEAUX]
این جدول، بازههای زمانی را که ثبتنام در RV امکانپذیر است، فهرست میکند:
![]() |
![]() |
- ID: شمارهای که جایگاه زمانی را شناسایی میکند – کلید اصلی جدول (ردیف ۸)
- VERSION: عددی که نسخهٔ سطر در جدول را شناسایی میکند. این عدد هر بار که تغییری در سطر ایجاد شود، یک واحد افزایش مییابد.
- ID_MEDECIN: شمارهای که پزشک مربوط به این اسلات را شناسایی میکند – کلید خارجی روی ستون MEDECINS (ID).
- HDEBUT: زمان شروع اسلات
- MDEBUT: دقیقه شروع اسلات
- HFIN: زمان پایان اسلات
- MFIN: دقیقه پایان اسلات
ردهی دوم جدول [CRENEAUX] (به [1] بالا مراجعه کنید) نشان میدهد که اسلات شمارهٔ ۲ از ساعت ۸:۲۰ شروع و در ساعت ۸:۴۰ پایان مییابد و به پزشک شمارهٔ ۱ اختصاص دارد. (خانم ماری PELISSIER).
8.3.4. جدول [RV]
ورودیهای 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 همزمان برای یک پزشک ثبت شدهاند. از دیدگاه برنامهنویسی جاوا، درایور JDBC در پایگاه داده هنگام وقوع این امر، یک SQLException را فعال میکند.
ورودی مربوط به id با مقدار ۳ (رجوع شود به [1] در بالا) نشان میدهد که یک RV برای اسلات شماره ۲۰ و مشتری شماره ۴ در تاریخ ۲۳ اوت ۲۰۰۶ رزرو شده است. جدول [CRENEAUX] به ما میگوید که نوبت شمارهٔ ۲۰ معادل بازهٔ زمانی ۱۶:۲۰ تا ۱۶:۴۰ است و متعلق به پزشک شمارهٔ ۱ (خانم ماری PELISSIER) میباشد. جدول [CLIENTS] نشان میدهد که مشتری شمارهٔ ۴ خانم بریژیت BISTROU است.
8.3.5. ایجاد پایگاه داده
برای ایجاد پایگاه داده [dbrdvmedecins]، یک اسکریپت [dbrdvmedecins.sql] همراه با مثالهای این سند [1-3] ارائه شده است:
![]() |
ما از ابزار [PhpMyAdmin] از WampServer استفاده میکنیم:
![]() |
- در [1]، ابزار [phpMyAdmin] را از WampServer انتخاب کنید؛
- در [2]، گزینه [Importer] را انتخاب کنید؛
![]() |
- در [3]، فایل [database/dbrdvmedecins.sql] را انتخاب کنید؛
- برای [4]، آن را اجرا کنید؛
- در [5]، پایگاه داده ایجاد میشود.
8.4. سرویس وب / jSON
![]() |
در معماری فوق، اکنون به ساخت سرویس وب / jSON با استفاده از فریمورک Spring MVC میپردازیم. ما آن را در چندین مرحله خواهیم نوشت:
- ابتدا، لایههای [métier] و [DAO] (Data Access Object). در اینجا از Spring Data استفاده خواهیم کرد؛
- سپس سرویس وب jSON بدون احراز هویت. در اینجا از Spring MVC استفاده خواهیم کرد؛
- سپس با استفاده از Spring Security، مؤلفه احراز هویت را اضافه خواهیم کرد.
متن زیر بازتولید سند [http://tahe.developpez.com/angularjs-spring4/] است، با چند تغییر.
8.4.1. مقدمهای بر 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]، پروژه نهایی.
8.4.1.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.1.10.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>
- خطوط ۵–۹: تعریف یک پروژه Maven والد. این بخش بیشتر وابستگیهای پروژه را تعریف میکند. این وابستگیها ممکن است کافی باشند، که در این صورت هیچ وابستگی دیگری اضافه نمیشود، یا ممکن است کافی نباشند، که در این صورت وابستگیهای missing اضافه میشوند؛
- خطوط ۱۲–۱۵: وابستگی به [spring-boot-starter-data-jpa] را تعریف میکند. این آرتیکفکت شامل کلاسهای Spring Data است؛
- خطوط 16–19: یک وابستگی به SGBD و H2 تعریف کنید که امکان ایجاد و مدیریت پایگاههای داده در حافظه را فراهم میکنند.
بیایید نگاهی به کلاسهای ارائهشده توسط این وابستگیها بیندازیم:
![]() | ![]() | ![]() |
تعداد زیادی از آنها وجود دارد:
- برخی به اکوسیستم Spring (همانهایی که با «spring» شروع میشوند) تعلق دارند؛
- سایر متعلق به اکوسیستم هایبرنیت (hibernate, jboss) هستند که ما در اینجا پیادهسازی JPA آن را استفاده میکنیم؛
- سایر کتابخانهها مربوط به تست هستند (junit, hamcrest);
- سایر کتابخانههای لاگگیری هستند (log4j، logback، slf4j);
ما همهٔ آنها را نگه میداریم. برای یک برنامهٔ تولیدی، باید فقط آنهایی را که ضروری هستند نگه داریم.
در خط ۲۶ فایل [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>
خطوط ۶–۹: افزونه [spring-boot-maven-plugin] برای تولید فایل JAR قابل اجرای برنامه استفاده میشود. بنابراین خط ۲۶ از فایل [pom.xml] کلاس قابل اجرا را در این JAR مشخص میکند.
8.4.1.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] نمایانگر یک سطر در یک جدول پایگاه داده است.
- خط ۸: anotation JPA، که به این معنی است که پایداری نمونههای [Customer] (ایجاد، خواندن، بهروزرسانی، حذف) توسط یک پیادهسازی JPA مدیریت خواهد شد. طبق وابستگیهای Maven، میتوان دید که پیادهسازی JPA / Hibernate در حال استفاده است؛
- خطوط ۱۱–۱۲: حاشیهنویسیهای JPA که فیلد [id] را با کلید اصلی جدول [Customer] مرتبط میکنند. خط ۱۲ نشان میدهد که پیادهسازی JPA از روش تولید کلید اصلی خاص SGBD مورد استفاده، در این مورد H2، استفاده خواهد کرد؛
هیچ یادداشت دیگری برای JPA وجود ندارد. بنابراین از مقادیر پیشفرض استفاده خواهد شد:
- جدول [Customer] به نام کلاس نامگذاری خواهد شد، یعنی [Customer]؛
- ستونهای این جدول به نام فیلدهای کلاس نامگذاری خواهند شد: [id, firstName, lastName]، با در نظر گرفتن اینکه در نامهای ستون جدول به حروف بزرگ و کوچک توجه نمیشود؛
شایان ذکر است که پیادهسازی JPA هرگز نامگذاری نمیشود.
8.4.1.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);
}
بنابراین این یک رابط است و نه یک کلاس (خط ۷). این رابط، رابط [CrudRepository] را که یک رابط Spring Data است (خط ۵)، گسترش میدهد. این رابط با دو نوع پارامترگذاری شده است: اولی نوع عناصر مدیریتشده است که در این مورد نوع [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 انجام داد، تعریف میکند:
- خط ۸: متد save اجازه میدهد یک موجودیت T در پایگاه داده پایدارسازی شود. این متد موجودیت را با استفاده از کلید اصلی که توسط SGBD به آن اختصاص داده شده است، پایدارسازی میکند. همچنین اجازه میدهد یک موجودیت T، که با کلید اصلی id شناسایی میشود، بهروزرسانی شود. انتخاب بین این دو اقدام به مقدار کلید اصلی id بستگی دارد: اگر مقدار آن null باشد، عملیات پایداریسازی انجام میشود؛ در غیر این صورت، عملیات بهروزرسانی انجام میشود؛
- خط ۱۰: همین امر صدق میکند، اما برای یک لیست از اشیاء؛
- خط ۱۲: متد findOne برای بازیابی یک موجودیت T که با کلید اصلی id شناسایی شده است، استفاده میشود؛
- خط ۲۲: متد حذف برای حذف یک موجودیت T که با کلید اصلی آن id شناسایی شده است، استفاده میشود؛
- خطوط ۲۴–۲۸: گونههای متد [delete]؛
- خط ۱۶: متد [findAll] تمام انتیتهای ذخیرهشده T را بازیابی میکند؛
- خط ۱۸: مشابه مورد قبل، اما محدود به آن انتیتهایی که فهرستی از شناسهها برای آنها ارسال شده است؛
بیایید به رابط [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);
}
- خط ۹ به شما امکان میدهد یک [Customer] را با نام [lastName] آن بازیابی کنید؛
و این تمام چیزی است که در مورد لایه [DAO] وجود دارد. هیچ کلاس پیادهسازیای برای رابط قبلی وجود ندارد. این توسط [Spring Data] در زمان اجرا تولید میشود. متدهای رابط [CrudRepository] بهطور خودکار پیادهسازی میشوند. در مورد متدهای اضافه شده به رابط [CustomerRepository]، بستگی دارد. بیایید به تعریف [Customer] بازگردیم:
private long id;
private String firstName;
private String lastName;
متد در خط ۹ بهطور خودکار توسط [Spring Data] پیادهسازی میشود زیرا به فیلد [lastName] (خط ۳) از [Customer] ارجاع میدهد. وقتی Spring Data با متد [findBySomething] در رابطی که باید پیادهسازی شود مواجه میشود، آن را با استفاده از پرسوجوی زیر JPQL (زبان پرسوجوی پایداری جاوا) پیادهسازی میکند:
بنابراین نوع 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] را با استفاده از یک رابط ساده پیادهسازی کنیم.
8.4.1.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();
}
}
- خط ۱۰: نشان میدهد که این کلاس برای پیکربندی Spring استفاده میشود. نسخههای اخیر Spring در واقع میتوانند به جای XML، در جاوا پیکربندی شوند. هر دو روش میتوانند همزمان استفاده شوند. در کد یک کلاس که دارای anotation [Configuration] است، معمولاً بیینهای Spring، یعنی تعریف کلاسهایی که باید نمونهسازی شوند، یافت میشود. در اینجا، هیچ بیینی تعریف نشده است. باید توجه داشت که هنگام کار با SGBD، بیینهای مختلف Spring باید تعریف شوند:
- یک [EntityManagerFactory]، که پیادهسازی JPA مورد استفاده را تعریف میکند،
- یک [DataSource] که منبع دادهای را که باید استفاده شود، تعریف میکند،
- یک بیون [TransactionManager] که مدیر تراکنش مورد استفاده را تعریف میکند؛
در اینجا، هیچکدام از این بیینها تعریف نشدهاند.
- خط ۱۱: anotation [EnableAutoConfiguration] یک anotation از پروژه [Spring Boot] (خطوط ۵–۶) است. این anotation از طریق کلاس [SpringApplication] (خط 16) به Spring Boot دستور میدهد که برنامه را بر اساس کتابخانههای موجود در classpath پیکربندی کند. از آنجا که کتابخانههای Hibernate در classpath قرار دارند، bean با شناسه [entityManagerFactory] با استفاده از Hibernate پیادهسازی خواهد شد. از آنجایی که کتابخانههای SGBD و H2 در مسیر کلاس (classpath) قرار دارند، بیون [dataSource] با استفاده از H2 پیادهسازی خواهد شد. در بیان [dataSource]، نام کاربری و رمز عبور نیز باید تعریف شوند. در اینجا، Spring Boot از مدیر پیشفرض در H2 استفاده خواهد کرد که رمز عبور ندارد. از آنجا که کتابخانه [spring-tx] در مسیر کلاسها قرار دارد، از مدیر تراکنش Spring استفاده خواهد شد.
علاوه بر این، پوشهای که حاوی کلاس [Application] است، برای یافتن بیونهایی که بهطور ضمنی توسط Spring شناسایی میشوند یا بهطور صریح توسط anotationهای Spring تعریف شدهاند، اسکن خواهد شد. بنابراین، کلاسهای [Customer] و [CustomerRepository] بررسی خواهند شد. از آنجا که کلاس اول دارای anotation [@Entity] است، بهعنوان یک انتیتا که توسط Hibernate مدیریت میشود طبقهبندی خواهد شد. از آنجا که کلاس دوم interface [CrudRepository] را extend میکند، بهعنوان یک Spring bean ثبت خواهد شد.
بیایید خطوط ۱۶–۱۷ کد را بررسی کنیم:
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
- خط ۱۶: متد استاتیک [run] از کلاس [SpringApplication] در پروژه Spring Boot اجرا میشود. پارامتر آن کلاسی است که دارای انوتیشن [Configuration] یا [EnableAutoConfiguration] است. سپس همه چیزهایی که قبلاً توضیح داده شد، انجام میشود. نتیجه یک کانکست برنامه Spring است، یعنی مجموعهای از بینها که توسط Spring مدیریت میشوند؛
- خط ۱۷: ما یک بین (bean) را که رابط [CustomerRepository] را پیادهسازی میکند، از این زمینه اسپرینگ درخواست میکنیم. در اینجا، ما کلاسی را که توسط Spring Data برای پیادهسازی این رابط تولید شده است، بازیابی میکنیم.
عملیات زیر صرفاً متدهای بیانی را که رابط [CustomerRepository] را پیادهسازی میکنند، فراخوانی میکنند. توجه کنید که در خط ۵۰، کانکست بسته میشود. خروجی کنسول به شرح زیر است:
- خطوط ۱–۸: لوگوی پروژه Spring Boot؛
- خط ۹: کلاس [hello.Application] اجرا میشود؛
- خط ۱۰: [AnnotationConfigApplicationContext] کلاسی است که رابط [ApplicationContext] اسپرینگ را پیادهسازی میکند. این یک کانtejner بین (bean container) است؛
- خط ۱۱: بیون [entityManagerFactory] با استفاده از کلاس [LocalContainerEntityManagerFactory]، یک کلاس Spring، پیادهسازی شده است؛
- خط ۱۵: [Hibernate] ظاهر میشود. این پیادهسازی، JPA، است که انتخاب شده است؛
- خط ۱۹: یک گویش Hibernate، به طور خاص نوع SQL، قرار است با SGBD استفاده شود. در اینجا، گویش [H2Dialect] نشان میدهد که Hibernate با SGBD و H2 کار خواهد کرد؛
- خطوط ۲۱–۲۲: پایگاه داده ایجاد میشود. جدول [CUSTOMER] ایجاد میشود. این بدان معناست که Hibernate برای تولید جدولها از تعریفهای JPA پیکربندی شده است؛ در اینجا، تعریف JPA از کلاس [Customer]؛
- خطوط ۲۷–۳۱: پنج مشتری درج شدند؛
- خطوط 33–635: نتیجه متد [findOne] اینفریس؛
- خطوط ۳۷–۴۰: نتایج متد [findByLastName]؛
- خطوط ۴۱ به بعد: لاگهای مربوط به بستهشدن کانکست Spring.
8.4.1.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.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!--تراکنشهای Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- اسپرینگ ORM -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.7.1.RELEASE</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>1.1.10.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>
<!-- Commons 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>
...
</project>
- خطوط ۲–۱۸: کتابخانههای اصلی Spring؛
- خطوط ۱۹–۲۹: کتابخانههای Spring برای مدیریت تراکنشهای پایگاه داده؛
- خطوط ۳۰–۳۵: کتابخانه Spring برای کار با ORM (نقشهبردار شیء-رابطهای)؛
- خطوط ۳۶–۴۱: Spring Data برای دسترسی به پایگاه داده استفاده میشود؛
- خطوط ۴۲–۴۷: Spring Boot برای راهاندازی برنامه؛
- خطوط ۵۴–۵۹: SGBD H2;
- خطوط 60–70: پایگاههای داده اغلب با استخرهای اتصال باز استفاده میشوند که از باز و بسته کردن مکرر اتصالات جلوگیری میکند. در اینجا، پیادهسازی مورد استفاده متعلق به [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();
}
}
- خط ۱۲: کلاس [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;
}
}
- خط ۲۲: آناوتیشن [@Configuration] کلاس [Config] را بهعنوان یک کلاس پیکربندی Spring تعیین میکند؛
- خط ۲۱: anotation [@EnableJpaRepositories] دایرکتوریهای حاوی رابطهای Spring Data با نام [CrudRepository] را مشخص میکند. این رابطها به کامپوننتهای Spring تبدیل شده و در بستر آن در دسترس خواهند بود؛
- خط ۲۰: آناوتیشن [@EnableTransactionManagement] مشخص میکند که متدهای اینترفیسهای [CrudRepository] باید در داخل یک تراکنش اجرا شوند؛
- خط ۱۹: آناوتیشن [@EntityScan] به شما امکان میدهد دایرکتوریهایی را که باید در آنها به دنبال اشیاء JPA جستجو شود، مشخص کنید. در اینجا غیرفعال شده است، زیرا این اطلاعات به صراحت در خط ۵۰ ارائه شده است. این حاشیهنویسی باید در صورتی که از حالت [@EnableAutoConfiguration] استفاده میشود و موجودیتهای JPA در همان پوشه کلاس پیکربندی قرار ندارند، موجود باشد؛
- خط ۱۸: آناوتیشن [@ComponentScan] برای فهرست کردن پوشههایی استفاده میشود که باید در آنها به دنبال کامپوننتهای Spring گشت. کامپوننتهای Spring کلاسهایی هستند که با آنوتیشنهای Spring مانند @Service، @Component، @Controller و غیره تگ شدهاند. در اینجا، آنوتیشنهای دیگری به جز آنهایی که در کلاس [Config] تعریف شدهاند وجود ندارد، بنابراین آنوتیشن کامنت شده است؛
- خطوط ۲۵–۳۳: منبع داده، پایگاه داده H2 را تعریف میکنند. این @Bean annotation در خط ۲۵ است که شیء ایجادشده توسط این متد را به یک کامپوننت مدیریتشده توسط Spring تبدیل میکند. نام متد میتواند در اینجا هر چیزی باشد. با این حال، اگر EntityManagerFactory در خط 47 وجود نداشته باشد و از طریق پیکربندی خودکار تعریف شده باشد، باید متد [dataSource] نامیده شود؛
- خط ۲۹: پایگاه داده با نام [demo] نامگذاری شده و در پوشه پروژه ایجاد خواهد شد؛
- خطوط ۳۶–۴۳: پیادهسازی مورد استفاده JPA را تعریف میکنند، در این مورد یک پیادهسازی Hibernate. نام متد در اینجا میتواند هر چیزی باشد؛
- خط ۳۹: هیچ لاگی برای SQL ثبت نمیشود؛
- خط ۳۰: پایگاه داده در صورت عدم وجود ایجاد خواهد شد؛
- خطوط ۴۶–۵۴: متد EntityManagerFactory را که به پایداری JPA رسیدگی میکند، تعریف کنید. نام این متد باید [entityManagerFactory] باشد؛
- خط ۴۷: متد دو پارامتر از همان نوع دو بیانی که قبلاً تعریف شدهاند را دریافت میکند. اینها سپس توسط اسپرینگ به عنوان پارامترهای متد نمونه سازی و تزریق میشوند؛
- خط ۴۹: پیادهسازی JPA را که باید استفاده شود، مشخص میکند؛
- خط ۵۰: دایرکتوریهایی را مشخص میکند که در آنها میتوان اشیاء JPA را یافت؛
- خط ۵۱: منبع دادهای را که باید مدیریت شود، مشخص میکند؛
- خطوط ۵۷–۶۲: مدیر تراکنش. متد باید با نام [transactionManager] نامگذاری شود. این متد، بین (bean) از خطوط ۴۶–۵۴ را به عنوان پارامتر دریافت میکند؛
- خط ۶۰: مدیر تراکنش با 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();
}
}
- خط ۱۵: کلاس پیکربندی [Config] اکنون توسط کلاس Spring با نام [AnnotationConfigApplicationContext] استفاده میشود. در خط ۵ مشاهده میکنیم که دیگر هیچ وابستگی به Spring Boot وجود ندارد.
اجرای کد نتایج مشابهی با قبل تولید میکند.
8.4.1.6. ایجاد یک آرشیو قابل اجرا
برای ایجاد یک آرشیو اجرایی از پروژه، مراحل زیر را دنبال کنید:
![]() |
- در [1]: یک پیکربندی اجرا ایجاد کنید؛
- در [2]: از نوع [Java Application]
- در [3]: پروژهٔ اجرایی را مشخص کنید (از دکمهٔ Browse استفاده کنید)؛
- در [4]: کلاس قابل اجرا را مشخص میکند؛
- [5]: نام پیکربندی اجرا – میتواند هر چیزی باشد؛
![]() |
- در [6]: پروژه صادر میشود؛
- در [7]: به صورت یک آرشیو اجرایی JAR؛
- در [8]: مسیر و نام فایل اجرایی که باید ایجاد شود را مشخص میکند؛
- در [9]: نام پیکربندی اجرای ایجاد شده در [5];
پس از انجام این کار، یک کنسول را در پوشهای که آرشیو اجرایی در آن قرار دارد باز کنید:
آرشیو به شرح زیر اجرا میشود:
.....\dist>java -jar gs-accessing-data-jpa-2.jar
خروجیهای نمایشدادهشده در کنسول به شرح زیر است:
8.4.1.7. یک پروژه جدید Spring Data ایجاد کنید
برای ایجاد یک قالب پروژه Spring Data، مراحل زیر را دنبال کنید:
![]() |
- در [1]، یک پروژه جدید ایجاد کنید؛
- در [2]: از نوع [Spring Starter Project];
- پروژه تولیدشده یک پروژه Maven خواهد بود. در [3]، نام گروه پروژه را مشخص کنید؛
- در [4]: نام محصول (در این مورد یک فایل JAR) را که هنگام ساخت پروژه ایجاد میشود، مشخص کنید؛
- در [5]: بستهٔ کلاس قابل اجرا که در پروژه ایجاد میشود، مشخص میگردد؛
- در [6]: نام اکلیپس پروژه – این میتواند هر چیزی باشد (لازم نیست که با [4] یکسان باشد)؛
- در [7]: این مشخص میکند که یک پروژه با یک لایه [JPA] باید ایجاد شود. وابستگیهای مورد نیاز برای چنین پروژهای سپس در فایل [pom.xml] گنجانده خواهند شد؛
![]() |
- در [8]: پروژه ایجاد میشود؛
فایل [pom.xml] وابستگیهای مورد نیاز برای پروژه JPA را در بر میگیرد:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.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>
- خطوط ۹–۱۲: وابستگیهای مورد نیاز برای JPA – شامل [Spring Data] خواهد بود؛
- خطوط ۱۳–۱۷: وابستگیهای مورد نیاز برای تستهای 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() {
}
}
- خط ۹: anotation [@SpringApplicationConfiguration] امکان استفاده از فایل پیکربندی [Application] را فراهم میکند. بنابراین کلاس تست از تمام beanهای تعریفشده در این فایل بهرهمند خواهد شد؛
- خط ۸: آناوتیشن [@RunWith] یکپارچهسازی Spring با JUnit را فعال میکند: این کلاس قادر خواهد بود بهعنوان یک تست JUnit اجرا شود. [@RunWith] یک anotation JUnit (خط ۴) است، در حالی که کلاس [SpringJUnit4ClassRunner] یک کلاس Spring (خط ۶) است؛
اکنون که اسکلت برنامه JPA را داریم، میتوانیم آن را تکمیل کنیم تا لایه پایداری سرور را برای برنامه مدیریت قرار ملاقاتهایمان بنویسیم.
8.4.2. پروژه سرور اکلیپس
![]() |
![]() |
اجزای اصلی پروژه به شرح زیر هستند:
- [pom.xml]: فایل پیکربندی Maven پروژه؛
- [rdvmedecins.entities]: انتیتههای JPA؛
- [rdvmedecins.repositories]: رابطهای Spring Data برای دسترسی به انتیتههای JPA؛
- [rdvmedecins.metier]: لایه [métier]؛
- [rdvmedecins.domain]: اِنتِیتیهایی که توسط لایه [métier] مدیریت میشوند؛
- [rdvmdecins.config]: کلاسهای پیکربندی لایه پایداری؛
- [rdvmedecins.boot]: یک برنامه کنسول پایه؛
8.4.3. پیکربندی 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.2.6.RELEASE</version>
</parent>
<dependencies>
<!--Spring Data JPA -->
<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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- راننده JDBC / MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Tomcat JDBC -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
<!-- نقشهکش jSON -->
<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>rdvmedecins.boot.Boot</start-class>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<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>
- خطوط ۸–۱۲: پروژه بر اساس پروژه والد [spring-boot-starter-parent] ساخته شده است. برای وابستگیهایی که از قبل در پروژه والد موجود هستند، هیچ نسخهای مشخص نشده است. از نسخهای که در پروژه والد تعریف شده است استفاده خواهد شد. سایر وابستگیها به صورت عادی اعلام شدهاند؛
- خطوط ۱۵–۱۸: برای Spring Data؛
- خطوط ۲۰–۲۴: برای تستهای JUnit؛
- خطوط 26–29: برای کتابخانه Spring Security، که لایه [DAO] آن از یکی از کلاسهای رمزگذاری رمز عبور استفاده میکند؛
- خطوط ۳۱–۳۴: درایور JDBC برای SGBD و MySQL5;
- خطوط ۳۶–۳۹: استخر اتصال Tomcat JDBC. یک استخر اتصال، اتصالات باز به پایگاه داده را گروهی در خود نگه میدارد. وقتی کد میخواهد یک اتصال را باز کند، یک اتصال را از استخر درخواست میکند. وقتی کد اتصال را میبندد، اتصال بسته نمیشود بلکه به استخر بازگردانده میشود. تمام این موارد از دیدگاه کد بهصورت شفاف اتفاق میافتد. این امر عملکرد را بهبود میبخشد، زیرا باز و بسته کردن مکرر یک اتصال زمانبر است. در اینجا، استخر اتصال به محض ایجاد شدن، تعداد مشخصی اتصال به پایگاه داده برقرار میکند. پس از آن، هیچ اتصالی باز یا بسته نمیشود، مگر اینکه تعداد اتصالات ذخیرهشده در استخر ناکافی باشد. در این صورت، استخر به طور خودکار اتصالات جدیدی ایجاد میکند؛
- خطوط ۴۱–۴۴: کتابخانه جکسون برای مدیریت jSON;
- خطوط ۴۶–۵۰: کتابخانه گوگل برای مدیریت مجموعهها؛
8.4.4. اشیاء 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.IDENTITY)
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) || entity==null) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return this.id.longValue() == other.id.longValue();
}
// گیرنده و تنظیمکننده
..
}
- خط ۱۱: انوتیشن [@MappedSuperclass] نشان میدهد که کلاس انوتهشده، والدِ انتیتهای JPA و [@Entity] است؛
- خطوط ۱۵–۱۷: کلید اصلی [id] را برای هر انتها تعریف کنید. این حاشیهنویسی [@Id] است که فیلد [id] را بهعنوان کلید اصلی تعیین میکند. توضیح [@GeneratedValue(strategy = GenerationType.IDENTITY)] نشان میدهد که مقدار این کلید اصلی توسط SGBD تولید میشود و حالت تولید [IDENTITY] اعمال میشود. برای SGBD و MySQL، این بدان معناست که کلیدهای اصلی توسط SGBD با استفاده از ویژگی [AUTO_INCREMENT] تولید خواهند شد
- خطوط ۱۸–۱۹: نسخهٔ هر انتها را تعریف میکنند. پیادهسازی JPA هر بار که انتها تغییر کند، این شمارهٔ نسخه را افزایش میدهد. این عدد برای جلوگیری از بهروزرسانی همزمان یک موجودیت توسط دو کاربر مختلف استفاده میشود: دو کاربر، U1 و U2، موجودیت E را با شماره نسخه برابر با V1 خواندند. U1، E را اصلاح میکند و این تغییر را در پایگاه داده ثبت میکند: شماره نسخه سپس به V1+1 تغییر مییابد. به نوبه خود E را اصلاح کرده و این تغییر را در پایگاه داده ثبت میکند: یک استثنا (exception) به وجود خواهد آمد زیرا نسخه آن (V1) با نسخه موجود در پایگاه داده (V1+1) متفاوت است؛
- خطوط ۲۹–۳۳: متد [build] برای مقداردهی اولیه به دو فیلد [AbstractEntity] استفاده میشود. این متد ارجاع به نمونه [AbstractEntity] را که به این ترتیب مقداردهی شده است، بازمیگرداند؛
- خطوط ۳۶–۴۴: متد [equals] کلاس بازتعریف شده است: دو شیء زمانی برابر در نظر گرفته میشوند که نام کلاس و شناسهی یکسانی داشته باشند؛
- خطوط ۲۱–۲۶: هنگام بازتعریف متد [equals] یک کلاس، متد [hashCode] آن نیز باید بازتعریف شود (خطوط ۲۱–۲۶). قاعده این است که دو موجودیتی که توسط متد [equals] برابر تشخیص داده شدهاند، باید [hashCode] یکسانی داشته باشند. در اینجا، [hashCode] یک موجودیت برابر با کلید اصلی آن [id] است. [hashCode] یک کلاس، بهویژه در مدیریت فرهنگلغطهایی که مقادیرشان نمونههایی از آن کلاس هستند، استفاده میشود؛
کلاس [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);
}
// گیرندهها و تنظیمکنندهها
...
}
- خط ۶: نشانهگذاری [@MappedSuperclass] نشان میدهد که کلاس نشانهگذاریشده، والدِ موجودیتهای JPA و [@Entity] است؛
- خطوط ۱۰–۱۵: یک شخص دارای یک عنوان (Miss)، یک نام (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());
}
}
- ردیف ۶: کلاس یک موجودیت JPA است؛
- خط ۷: مرتبط با جدول [MEDECINS] در پایگاه داده؛
- خط ۸: موجوده [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());
}
}
- خط ۶: کلاس یک موجودیت JPA است؛
- خط ۷: مرتبط با جدول [CLIENTS] در پایگاه داده؛
- خط ۸: موجوده [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;
}
// متدهای setter و getter
...
}
- خط ۱۰: کلاس یک موجودیت JPA است؛
- خط ۱۱: مرتبط با جدول [CRENEAUX] در پایگاه داده؛
- خط ۱۲: موجودیت [Creneau] از موجودیت [AbstractEntity] مشتق شده و بنابراین شناسهی [id] و نسخهی [version] را به ارث میبرد؛
- خط 16: زمان شروع اسلات (14);
- خط ۱۷: دقایق شروع اسلات (۲۰);
- خط ۱۸: ساعت پایان اسلات (۱۴);
- خط ۱۹: دقایق پایانی اسلات (۴۰);
- خطوط ۲۲–۲۴: پزشکی که اسلات به او تعلق دارد. جدول [CRENEAUX] دارای یک کلید خارجی است که به جدول [MEDECINS] ارجاع میدهد. این رابطه توسط خطوط ۲۲–۲۴ نشان داده شده است؛
- ردیف ۲۲: شناسه [@ManyToOne] نشاندهنده یک رابطه چند به یک (بین اسلاتها و پزشک) است. ویژگی [fetch=FetchType.LAZY] مشخص میکند که وقتی یک موجودیت [Creneau] از زمینه پایداری درخواست میشود و باید از پایگاه داده بازیابی شود، موجودیت [Medecin] به همراه آن بازگردانده نمیشود. مزیت این حالت این است که موجودیت [Medecin] تنها در صورتی بازیابی میشود که توسعهدهنده آن را درخواست کند. این امر در مصرف حافظه صرفهجویی کرده و عملکرد را بهبود میبخشد؛
- خط ۲۳: نام ستون کلید خارجی در جدول [CRENEAUX] را مشخص میکند؛
- خطوط ۲۷–۲۸: کلید خارجی در جدول [MEDECINS];
- خط ۲۷: ستون [ID_MEDECIN] قبلاً در خط ۲۳ استفاده شده است. این بدان معناست که میتوان آن را به دو روش مختلف تغییر داد، که طبق استاندارد 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;
}
// گیرنده و تنظیمکننده
...
}
- خط ۱۴: کلاس یک موجودیت JPA است؛
- خط ۱۵: مرتبط با جدول [RV] در پایگاه داده؛
- خط ۱۶: موجوده [Rv] از موجوده [AbstractEntity] مشتق شده و بنابراین شناسهی [id] و نسخهی [version] را به ارث میبرد؛
- خط ۲۱: تاریخ قرار ملاقات؛
- خط ۲۰: نوع جاوا [Date] شامل هم تاریخ و هم زمان است. در اینجا مشخص شده است که فقط تاریخ استفاده میشود؛
- خطوط ۲۴–۲۶: مشتریای که این قرار ملاقات برای او تنظیم شده است. جدول [RV] دارای یک کلید خارجی است که به جدول [CLIENTS] ارجاع میدهد. این رابطه توسط خطوط ۲۴–۲۶ نشان داده شده است؛
- ردههای 29–31: بازه زمانی قرار ملاقات. جدول [RV] دارای یک کلید خارجی است که به جدول [CRENEAUX] ارجاع میدهد. این رابطه توسط ردههای 29–31 نشان داده شده است؛
- ردههای ۳۴–۳۵: کلید خارجی [idClient];
- ردههای ۳۶–۳۷: کلید خارجی [idCreneau];
8.4.5. لایه [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> {
}
- خط ۷: رابط [MedecinRepository] به سادگی متدها را از رابط [CrudRepository] ارث میبرد و هیچ متد دیگری اضافه نمیکند؛
رابط [ClientRepository] به شرح زیر است:
package rdvmedecins.repositories;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Client;
public interface ClientRepository extends CrudRepository<Client, Long> {
}
- خط ۷: رابط [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);
}
- خط ۸: رابط [CreneauRepository] متدها را از رابط [CrudRepository] به ارث میبرد؛
- خطوط ۱۰–۱۱: متد [getAllCreneaux] شیفتهای زمانی آزاد یک پزشک را بازیابی میکند؛
- خط ۱۱: پارامتر، شناسهٔ پزشک است. نتیجه، فهرستی از اسلاتهای قرار ملاقات در قالب یک شیء [Iterable<Creneau>] است؛
- خط ۱۰: تفسیر [@Query] برای مشخص کردن پرسوجوی JPQL (زبان پرسوجوی پایداری جاوا) که متد را پیادهسازی میکند، استفاده میشود. پارامتر [?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);
}
- خط ۱۰: رابط [RvRepository] متدها را از رابط [CrudRepository] ارث میبرد؛
- خطوط ۱۲–۱۳: متد [getRvMedecinJour] قرار ملاقاتهای پزشک را برای یک روز مشخص بازیابی میکند؛
- خط ۱۳: پارامترها شناسهٔ پزشک و روز هستند. نتیجه، فهرستی از قرارها در قالب یک شیء [Iterable<Rv>] است؛
- خط ۱۲: تفسیر [@Query] برای مشخص کردن پرسوجوی JPQL که متد را پیادهسازی میکند، استفاده میشود. پارامتر [?1] با پارامتر متد [idMedecin] جایگزین میشود و پارامتر [?2] با پارامتر متد [jour] جایگزین میشود. پرسوجوی زیر JPQL کافی نیست:
زیرا فیلدهای کلاس Rv، با نوعهای [Client] و [Creneau]، در حالت [FetchType.LAZY] بازیابی میشوند، که به این معنی است که برای بازیابی آنها باید صراحتاً درخواست شوند. این کار در پرسوجوی JPQL با استفاده از دستوری [left join fetch entité] انجام میشود که درخواست میکند برای بازیابی موجودیت مرجع، یک پیوند با جدولی که کلید خارجی به آن اشاره میکند برقرار شود؛
8.4.6. لایه [métier]
![]() |
![]() |
- [IMetier] رابط لایه [métier] است و [Metier] پیادهسازی آن است؛
- [AgendaMedecinJour] و [CreneauMedecinJour] دو موجودیت تجاری هستند؛
8.4.6.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;
// fields
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);
}
// گیرنده و تنظیمکننده
...
}
- خط ۱۲: بازه زمانی؛
- خط ۱۳: قرار ملاقات، در صورت وجود – در غیر این صورت 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;
// fields
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());
}
// گیرندهها و تنظیمکنندهها
...
}
- خط ۱۳: پزشک؛
- خط ۱۴: روز در دفترچه یادداشت؛
- خط ۱۵: ساعات کاری آن، با یا بدون قرار ملاقات؛
8.4.6.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) {
...
}
}
- خط ۲۴: anotation [@Service] یک anotation از Spring است که کلاس دارای آن را به یک کامپوننت تحت مدیریت Spring تبدیل میکند. یک کامپوننت ممکن است نامگذاری شود یا نشود. این مورد [métier] نام دارد؛
- خط ۲۵: کلاس [Metier] رابط [IMetier] را پیادهسازی میکند؛
- خط ۲۸: anotation [@Autowired] یک anotation از Spring است. مقدار فیلدی که به این روش anotation شده است توسط Spring با یک مرجع به یک component از Spring با نوع یا نام مشخص، initialize (inject) خواهد شد. در اینجا، آناوتیشن [@Autowired] نامی را مشخص نمیکند. بنابراین، تزریق مبتنی بر نوع انجام خواهد شد؛
- خط ۲۹: فیلد [medecinRepository] با یک مرجع به یک کامپوننت Spring از نوع [MedecinRepository] مقداردهی اولیه خواهد شد. این مرجع، به کلاس تولید شده توسط Spring Data برای پیادهسازی رابط [MedecinRepository] اشاره دارد که قبلاً در مورد آن بحث کردهایم؛
- خطوط ۳۰–۳۵: این فرآیند برای سه رابط دیگر مورد بررسی نیز تکرار میشود؛
- خطوط 39–41: پیادهسازی متد [getAllClients];
- خط ۴۰: از متد [findAll] در اینترفیس [ClientRepository] استفاده میکنیم. این متد یک نوع [Iterable<Client>] را برمیگرداند که ما آن را با استفاده از متد استاتیک [Lists.newArrayList] به [List<Client>] تبدیل میکنیم. کلاس [Lists] در کتابخانه Google Guava تعریف شده است. در [pom.xml]، این وابستگی وارد شده است:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
- خطوط ۳۸–۸۶: متدهای رابط [IMetier] با استفاده از کلاسهای لایه [DAO] پیادهسازی شدهاند؛
تنها متد در خط ۸۸ مختص لایه [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;
}
از خوانندگان دعوت میشود نظرات را مطالعه کنند. الگوریتم به شرح زیر است:
- تمام بازههای زمانی برای پزشک مشخصشده بازیابی میشوند؛
- تمام قرارهای ملاقات آنها را برای روز مشخص بازیابی میکنیم؛
- با استفاده از این دو اطلاعات، میتوانیم تشخیص دهیم که یک بازه زمانی آزاد است یا رزرو شده است؛
8.4.7. پیکربندی پروژه Spring
![]() |
کلاس [DomainAndPersistenceConfig] کل پروژه را پیکربندی میکند:
package rdvmedecins.config;
import javax.persistence.EntityManagerFactory;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
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;
@Configuration
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@ComponentScan(basePackages = { "rdvmedecins" })
public class DomainAndPersistenceConfig {
//بستههای موجودیت JPA
public final static String[] ENTITIES_PACKAGES = { "rdvmedecins.entities", "rdvmedecins.security" };
//منبع داده MySQL
@Bean
public DataSource dataSource() {
//منبع داده TomcatJdbc
DataSource dataSource = new DataSource();
// JDBC پیکربندی
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
dataSource.setUsername("root");
dataSource.setPassword("");
// اتصالات اولیه را باز کنید
dataSource.setInitialSize(5);
// نتیجه
return dataSource;
}
// فراهمکننده JPA هیبرنیت است
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
return hibernateJpaVendorAdapter;
}
//EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan(ENTITIES_PACKAGES);
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// مدیر تراکنش
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- خط 17: این یک کلاس پیکربندی Spring است؛
- خط ۱۸: بستههای حاوی رابطهای Spring Data [CrudRepository]. اینها به زمینه Spring اضافه خواهند شد؛
- خط ۱۹: تمام کلاسهای موجود در بسته [rdvmedecins] و زیرکلاسهای آن که دارای انوتیشن Spring هستند را به کانکست Spring اضافه میکند. در پکیج [rdvmdecins.metier]، کلاس [Metier]، با آنوتیشن [@Service]، پیدا شده و به کانکست اسپرینگ اضافه میشود؛
- خطوط 26–39: پیکربندی استخر اتصالات Tomcat JDBC (خط 5);
- خط ۳۶: استخر اتصالات بهطور پیشفرض ۵ اتصال باز خواهد داشت. این خط صرفاً برای اهداف نمایشی ارائه شده است. در مورد ما، یک اتصال کافی خواهد بود. اگر لایه [DAO] توسط چندین نخ (thread) استفاده شود، این خط ضروری خواهد بود. این وضعیت در ادامه پیش خواهد آمد، زمانی که لایه [DAO] به عنوان ستون فقرات یک برنامه وب عمل میکند که ذاتاً از ارائه همزمان به چندین کاربر پشتیبانی میکند؛
- خطوط ۴۲–۴۹: پیادهسازی مورد استفاده برای JPA، یک پیادهسازی Hibernate است؛
- خط ۴۵: هیچ لاگ SQL وجود ندارد؛
- خط ۴۶: بازتولید جدول انجام نمیشود؛
- خط ۴۷: SGBD مورد استفاده، MySQL است؛
- خطوط ۵۳–۶۱: EntityManagerFactory را از لایه JPA تعریف میکنند. از این شیء، شیء [EntityManager] به دست میآید که امکان انجام عملیات JPA را فراهم میکند؛
- خط ۵۷: بستهها(ی) حاوی انتیتهای JPA را مشخص میکند؛
- خط ۵۸: منبع دادهای را مشخص میکند که باید به لایه JPA متصل شود؛
- خطوط ۶۴–۶۹: مدیر تراکنش مرتبط با EntityManagerFactory پیشین. بهطور پیشفرض، متدهای رابطهای [CrudRepository] در Spring Data درون یک تراکنش اجرا میشوند. مبادله قبل از ورود به متد آغاز میشود و پس از خروج از آن (با commit یا rollback) تکمیل میشود؛
8.4.8. آزمایشها برای لایه [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);
}
}
}
- خط ۲۲: anotasyon [@SpringApplicationConfiguration] امکان استفاده از فایل پیکربندی [DomainAndPersistenceConfig] را که قبلاً مورد بحث قرار گرفت، فراهم میکند. بنابراین کلاس تست از تمام beans تعریفشده در این فایل بهرهمند میشود؛
- خط ۲۳: آناوتیشن [@RunWith] یکپارچهسازی Spring با JUnit را ممکن میسازد: این کلاس قادر خواهد بود بهعنوان یک تست JUnit اجرا شود. [@RunWith] یک آنوتیشن JUnit است (خط ۹)، در حالی که کلاس [SpringJUnit4ClassRunner] یک کلاس Spring است (خط ۱۲)؛
- خطوط ۲۶–۲۷: تزریق یک مرجع به لایه [métier] به کلاس تست؛
- بسیاری از تستها صرفاً بررسیهای بصری هستند:
- خطوط ۳۲–۳۳: فهرست مشتریان؛
- خطوط ۳۵–۳۶: فهرست پزشکان؛
- خطوط ۳۹–۴۰: فهرست اسلاتهای قرار ملاقات یک پزشک؛
- خط ۴۳: فهرست قرارهای ملاقات پزشک؛
- خط ۵۰: افزودن یک قرار ملاقات جدید. متد [ajouterRv] قرار ملاقات را با اطلاعات اضافی، یعنی کلید اصلی آن «id»، بازمیگرداند؛
- خط ۵۳: این کلید اصلی برای جستجو در پایگاه داده جهت یافتن قرار ملاقات استفاده میشود؛
- خط ۵۴: بررسی میکنیم که قرار ملاقاتی که جستجو شده و قرار ملاقاتی که پیدا شده یکسان هستند. توجه کنید که متد [equals] از انتیت [Rv] مجدداً تعریف شده است: دو قرار ملاقات زمانی برابر در نظر گرفته میشوند که شناسه یکسانی داشته باشند. در اینجا، این به ما نشان میدهد که قرار ملاقات اضافه شده واقعاً در پایگاه داده درج شده است؛
- خطوط ۶۱–۷۳: تلاشی برای افزودن همان قرار ملاقات برای بار دوم انجام میشود. این باید توسط 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 ;
خط ۸ بالا نشان میدهد که ترکیب [JOUR, ID_CRENEAU] باید منحصر به فرد باشد، که از رزرو دو قرار ملاقات در یک روز و در یک بازه زمانی یکسان جلوگیری میکند.
- خط ۷۳: بررسی میکنیم که واقعاً یک استثنا رخ داده است؛
- خط ۷۷: تقویم پزشکی که قرار ملاقاتی برای او اضافه شده است، بازیابی میشود؛
- خط ۷۹: بررسی میکنیم که قرار ملاقات اضافه شده واقعاً در دفتر برنامهریزی او موجود است؛
- خط ۸۲: حذف قرار ملاقات اضافه شده؛
- خط ۸۴: بازیابی قرار ملاقات حذفشده از پایگاه داده؛
- خط ۸۵: بررسی میکنیم که نشانگر null را بازیابی کردهایم، که بدین ترتیب نشان میدهد قرار ملاقاتی که به دنبالش بودیم وجود ندارد؛
آزمون با موفقیت اجرا میشود:
![]() |
8.4.9. برنامه کنسول
![]() |
برنامه کنسول ساده است. این برنامه نشان میدهد چگونه یک کلید خارجی را بازیابی کنیم:
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);
}
}
}
برنامه یک قرار ملاقات اضافه میکند و سپس بررسی میکند که آیا اضافه شده است.
- خط ۱۹: کلاس [SpringApplication] از کلاس پیکربندی [DomainAndPersistenceConfig] استفاده خواهد کرد؛
- خط ۲۰: حذف لاگهای راهاندازی برنامه؛
- خط ۲۲: کلاس [SpringApplication] اجرا میشود. این کلاس یک کانکست Spring را بازمیگرداند، یعنی لیست بیونهای ثبتشده؛
- خط 24: یک مرجع به باینی که رابط [IMetier] را پیادهسازی میکند، بازیابی میشود. بنابراین این یک مرجع به لایه [métier] است؛
- خطوط ۲۷–۳۱: یک قرار ملاقات جدید برای امروز، برای مشتری شماره ۱ در نوبت شماره ۱ اضافه میشود. مشتری و نوبت از ابتدا ایجاد شدهاند تا نشان دهند که فقط شناسهها استفاده میشوند. نسخه در اینجا مقداردهی اولیه شده است، اما میتوانستیم هر عددی را وارد کنیم. این عدد در اینجا استفاده نمیشود؛
- خط ۳۴: ما میخواهیم بدانیم کدام پزشک شیفت شماره ۱ را دارد. برای این کار، باید پایگاه داده را برای شیفت شماره ۱ پرسوجو کنیم. از آنجایی که در حالت [FetchType.LAZY] هستیم، پزشک همراه با شیفت بازگردانده نمیشود. با این حال، ما اطمینان حاصل کردهایم که یک فیلد [idMedecin] را در درون انتیت [Creneau] برای بازیابی کلید اصلی دکتر قرار دادهایم؛
- خط ۳۵: کلید اصلی پزشک را بازیابی میکنیم؛
- خط ۳۶: فهرست قرارهای دکتر نمایش داده میشود؛
خروجی کنسول به شرح زیر است:
8.4.10. مدیریت لاگ
لاگهای کنسول توسط دو فایل پیکربندی میشوند: [application.properties] و [logback.xml] [1]:
![]() |
فایل [application.properties] توسط فریمورک Spring Boot استفاده میشود. این فایل به شما امکان میدهد تا طیف گستردهای از پارامترها را برای تغییر مقادیر پیشفرض Spring Boot (http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html) تعریف کنید. محتوای آن به این صورت است:
logging.level.org.hibernate=OFF
spring.main.show-banner=false
- خط ۱: سطح لاگگیری Hibernate را کنترل میکند – در اینجا هیچ لاگی ثبت نمیشود
- خط ۲: کنترل میکند که آیا بنر Spring Boot نمایش داده شود یا خیر – در اینجا بنری وجود ندارد
فایل [logback.xml] فایل پیکربندی برای چارچوب لاگگیری [logback] است:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- کدگذارها بهطور پیشفرض دارای نوع ch.qos.logback.classic.encoder.PatternLayoutEncoder هستند. -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!--کنترل سطح لاگ -->
<root level="info"> <!--خاموش، اطلاعات، اشکالزدایی، هشدار -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- سطح عمومی لاگ توسط خط ۹ کنترل میشود – در اینجا، لاگها در سطح [info] ثبت میشوند؛
این کار نتیجه زیر را تولید میکند:
اگر سطح لاگ Hibernate را روی [info] تنظیم کنیم (بدون تغییر هیچ چیز دیگر):
logging.level.org.hibernate=INFO
spring.main.show-banner=false
این نتیجه زیر را به دست میدهد:
اگر سطح لاگگیری را روی [debug] تنظیم کنیم (بدون تغییر هیچ چیز دیگر):
logging.level.org.hibernate=DEBUG
spring.main.show-banner=false
این نتیجه زیر را به دست میدهد:
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Eagerly caching bean 'clientRepository' to allow for resolving potential circular references
10:35:13.522 [main] DEBUG o.s.b.f.annotation.InjectionMetadata - Processing injected element of bean 'clientRepository': PersistenceElement for public void org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.setEntityManager(javax.persistence.EntityManager)
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#6a2eea2a'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Invoking afterPropertiesSet() on bean with name '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#6a2eea2a'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#1ba05e38'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#1ba05e38'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#6c298dc'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'entityManagerFactory'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#6c298dc'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'jpaMappingContext'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Invoking afterPropertiesSet() on bean with name 'clientRepository'
10:35:13.522 [main] DEBUG o.s.o.j.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler - Creating new EntityManager for shared EntityManager invocation
10:35:13.522 [main] DEBUG o.s.o.jpa.EntityManagerFactoryUtils - Closing JPA EntityManager
10:35:13.522 [main] DEBUG o.s.o.j.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler - Creating new EntityManager for shared EntityManager invocation
10:35:13.522 [main] DEBUG o.s.o.jpa.EntityManagerFactoryUtils - Closing JPA EntityManager
10:35:13.522 [main] DEBUG o.s.aop.framework.JdkDynamicAopProxy - Creating JDK dynamic proxy: target source is org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$ThreadBoundTargetSource@723ed581
10:35:13.522 [main] DEBUG o.s.aop.framework.JdkDynamicAopProxy - Creating JDK dynamic proxy: target source is SingletonTargetSource for target object [org.springframework.data.jpa.repository.support.SimpleJpaRepository@796065aa]
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean 'clientRepository'
10:35:13.522 [main] DEBUG o.s.b.f.a.AutowiredAnnotationBeanPostProcessor - Autowiring by type from bean name 'métier' to bean named 'clientRepository'
...
8.4.11. لایه [web / jSON]
![]() |
![]() |
ما لایه [web / jSON] را در چند مرحله خواهیم ساخت:
- مرحله ۱: یک لایه وب کاربردی بدون احراز هویت؛
- مرحله ۲: پیادهسازی احراز هویت با استفاده از Spring Security؛
- مرحله ۳: پیادهسازی 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 خواهد بود که لزوماً به همان دامنه سرویس وب ما تعلق ندارد. بنابراین، به طور پیشفرض، این کلاینت نمیتواند به سرویس وب دسترسی داشته باشد، مگر اینکه مجاز به انجام این کار باشد. ما خواهیم دید چگونه؛
8.4.11.1. پیکربندی 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>istia.st.spring4.mvc</groupId>
<artifactId>rdvmedecins-webjson-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-server</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
</parent>
<dependencies>
<!--لایه وب Spring MVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- لایه تست -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- لایه DAO -->
<dependency>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
...
</project>
- خطوط ۱۲–۱۵: پروژهٔ والد Maven؛
- خطوط ۱۹–۲۲: وابستگیها برای یک پروژه Spring، MVC;
- خطوط ۲۴–۲۸: وابستگیها برای تستهای JUnit / Spring؛
- خطوط ۳۰–۳۴: وابستگیها به پروژه [métier, DAO, JPA]؛
8.4.11.2. رابط سرویس وب
![]() |
- در [1]، همانطور که در بالا نشان داده شده است، مرورگر فقط میتواند تعداد محدودی URL را با یک سینتکس خاص درخواست کند؛
- در [4]، یک پاسخ jSON دریافت میکند؛
پاسخهای سرویس وب ما همگی دارای فرمت یکسانی خواهند بود که مطابق با تبدیل jSON یک شیء از نوع [Response] به شرح زیر است:
package rdvmedecins.web.models;
import java.util.List;
public class Response<T> {
// ----------------- ویژگیها
//وضعیت تراکنش
private int status;
// هرگونه پیام خطا
private List<String> messages;
// بدنه پاسخ
private T body;
// سازندهها
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// گیرنده و تنظیمکننده
...
}
- خط ۷: کد خطای پاسخ ۰: OK، در غیر این صورت: KO;
- خط ۱۱: فهرستی از پیامهای خطا، در صورت وجود؛
- خط ۱۳: بدنه پاسخ؛
اکنون اسکرینشاتهایی را که رابط سرویس وب / jSON را نشان میدهند، ارائه میکنیم:
فهرست تمام بیماران در مطب [/getAllClients]
![]() |
فهرست تمام پزشکان در مطب پزشکی [/getAllMedecins]
![]() |
فهرست نوبتهای پزشک [/getAllCreneaux/{idMedecin}]
![]() |
فهرست نوبتهای پزشک [/getRvMedecinJour/{idMedecin}/{yyyy-mm-dd}
![]() |
دفترچه یادداشت پزشک [/getAgendaMedecinJour/{idMedecin}/{aaaa-mm-jj}]
![]() |
برای افزودن یا حذف یک قرار ملاقات، از افزونه کروم [Advanced Rest Client] استفاده میکنیم، زیرا این عملیات با استفاده از POST انجام میشوند.
افزودن قرار ملاقات [/ajouterRv]
![]() |
- در [0]، URL از سرویس وب؛
- در [1]، از متد POST استفاده میشود؛
- در [2]، متن jSON اطلاعات ارسالشده به سرویس وب به شکل {day, idClient, idCreneau} است؛
- در [3]، کلاینت به سرویس وب اطلاع میدهد که اطلاعاتی را در قالب jSON ارسال میکند؛
پاسخ سپس به شرح زیر است:
![]() |
- در [4]: کلاینت هدر را ارسال میکند که نشان میدهد دادهای که ارسال میکند در قالب jSON است؛
- به [5]: سرویس وب پاسخ میدهد که آن نیز در حال ارسال jSON است؛
- در [6]: پاسخ سرویس وب jSON است. فیلد [body] حاوی فرمت jSON قرار ملاقات افزودهشده است؛
وجود قرار ملاقات جدید قابل تأیید است:
![]() |
شناسه قرار ملاقات را یادداشت کنید: [50]. ما قصد داریم همین را حذف کنیم.
حذف یک قرار ملاقات [/supprimerRv]
![]() |
- به [1]، URL از سرویس وب؛
- به [2]، روش POST استفاده میشود؛
- در [3]، متن jSON اطلاعات ارسالشده به سرویس وب در قالب {idRv} است؛
- در [4]، کلاینت به سرویس وب اطلاع میدهد که در حال ارسال اطلاعات jSON برای آن است؛
پاسخ سپس به شرح زیر است:
![]() |
- در [5]: فیلد [status] روی 0 تنظیم شده است، که بدین ترتیب موفقیت عملیات را نشان میدهد؛
حذف قرار ملاقات را میتوان تأیید کرد:
![]() |
همانطور که در بالا نشان داده شده است، قرار ملاقات برای بیمار [Mme GERMAIN] دیگر وجود ندارد.
سرویس وب همچنین امکان بازیابی اشیاء را بر اساس شناسه آنها فراهم میکند:
![]() |
![]() |
![]() |
![]() |
تمام این URL توسط کنترلکننده [RdvMedecinsController] پردازش میشوند که بهزودی آن را معرفی خواهیم کرد.
8.4.11.3. پیکربندی سرویس وب
![]() |
کلاس پیکربندی [AppConfig] به شرح زیر است:
package rdvmedecins.web.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
}
- خط ۱۲: کلاس [AppConfig] کل برنامه را پیکربندی میکند؛
- خط ۹: کلاس [AppConfig] یک کلاس پیکربندی Spring است؛
- خط ۱۰: کامپوننتهای Spring باید در پکیج [rdvmedecins.web] و زیرپکیجهای آن جستجو شوند. به این ترتیب، کامپوننتهای زیر کشف خواهند شد:
- [@RestController RdvMedecinsController] در بسته [rdvmedecins.web.controllers]؛
- [@Component ApplicationModel] در بسته [rdvmedecins.web.models]؛
- خط ۱۱: کلاس [DomainAndPersistenceConfig] وارد میشود؛ این کلاس پروژه [rdvmedecins-metier-dao] را پیکربندی میکند تا دسترسی به بی́nهای آن پروژه را فراهم کند؛
- خط ۱۱: کلاس [SecurityConfig] امنیت وباپلیکیشن را پیکربندی میکند. فعلاً این را نادیده میگیریم؛
- خط ۱۱: کلاس [WebConfig] لایه [web / jSON] را پیکربندی میکند؛
کلاس [WebConfig] به شرح زیر است:
package rdvmedecins.web.config;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@Configuration
@EnableWebMvc
public class WebConfig {
//پیکربندی DispatcherServlet برای سربرگها CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// نقشهسازها jSON
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(
new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter", creneauFilter));
return jsonMapperLongRv;
}
@Bean
public ObjectMapper jsonMapperShortRv() {
ObjectMapper jsonMapperShortRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
return jsonMapperShortRv;
}
}
- خطوط ۲۰–۲۵: تعریف کردن bean [dispatcherServlet]. کلاس [DispatcherServlet] servlet برای فریمورک Spring MVC است. این کلاس بهعنوان [FrontController] عمل میکند: درخواستهای ارسالشده به سایت Spring (MVC) را رهگیری کرده و آنها را به یکی از کنترلرهای سایت مسیریابی میکند؛
- خط ۲۲: نمونهسازی کلاس؛
- خط ۲۳: این خط را میتوان فعلاً نادیده گرفت؛
- خطوط ۲۷–۳۰: سرولت [dispatcherServlet] تمام درخواستهای URL را مدیریت میکند؛
- خطوط ۲۷–۳۰: فعالسازی سرور Tomcat تعبیهشده در وابستگیهای پروژه. این سرور روی پورت ۸۰۸۰ اجرا خواهد شد؛
- خطوط ۳۸–۶۷: چهار مپر jSON که با فیلترهای مختلف jSON پیکربندی شدهاند؛
- خطوط ۳۸–۴۱: یک مپر jSON بدون فیلتر؛
- خطوط ۴۳–۴۹: مپر jSON [jsonMapperShortCreneau] یک شیء [Creneau] را سریالیزه/دسریالیزه میکند در حالی که فیلد [Creneau.medecin] را نادیده میگیرد؛
- خطوط ۵۱–۵۹: مبدل jSON [jsonMapperLongRv] یک شیء [Rv] را سریالیزه/دسریالیزه میکند در حالی که فیلد [Rv.creneau.medecin] را نادیده میگیرد؛
- خطوط ۶۱–۶۷: مپر jSON [jsonMapperShortRv] سریالیزه میکند / یک شیء [Rv] را سریالیزه/دسریالیزه میکند، در حالی که فیلدهای [Rv.creneau] و [Rv.client] را نادیده میگیرد؛
8.4.11.4. کلاس [ApplicationModel]
![]() |
کلاس [ApplicationModel] دو هدف را برآورده میکند:
- به عنوان یک کش برای ذخیره فهرست پزشکان و بیماران (کلاینتها)؛
- بهعنوان یک رابط واحد برای کنترلکنندهها؛
package rdvmedecins.web.models;
import java.util.Date;
import java.util.List;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
import rdvmedecins.web.helpers.Static;
@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;
//دادههای پیکربندی
private boolean CORSneeded = false;
private boolean secured = false;
@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(long idRv) {
métier.supprimerRv(idRv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
return métier.getAgendaMedecinJour(idMedecin, jour);
}
// گیرندهها و تنظیمکنندهها
public boolean isCORSneeded() {
return CORSneeded;
}
public boolean isSecured() {
return secured;
}
}
- خط ۱۹: تگ @[@Component] کلاس [ApplicationModel] را به یک کامپوننت Spring تبدیل میکند. همانند تمام کامپوننتهای Spring که تاکنون دیدهایم (به جز @Controller)، تنها یک شیء از این نوع ایجاد خواهد شد (singleton)؛
- خط ۲۰: کلاس [ApplicationModel] رابط [IMetier] را پیادهسازی میکند؛
- خطوط ۲۳–۲۴: یک مرجع به لایه [métier] توسط Spring تزریق میشود؛
- خط ۳۴: آناوتیشن [@PostConstruct] تضمین میکند که متد [init] بلافاصله پس از instantiate شدن کلاس [ApplicationModel] اجرا شود؛
- خطوط ۳۸–۳۹: فهرست پزشکان و مشتریان از لایه [métier] بازیابی میشوند؛
- خط ۴۱: اگر استثنا رخ دهد، پیامهای پشته استثنا در فیلدِ خط ۱۷ ذخیره میشوند؛
معماری لایه وب به شرح زیر تکامل مییابد:
![]() |
- در [2b]، متدهای کنترلکننده(ها) با نمونهی واحد [ApplicationModel] ارتباط برقرار میکنند؛
این استراتژی انعطافپذیری را در مدیریت کش فراهم میکند. در حال حاضر، اسلاتهای قرار ملاقات پزشکان در کش ذخیره نمیشوند. برای ذخیره کردن آنها، به سادگی کلاس [ApplicationModel] را اصلاح کنید. این کار هیچ تأثیری بر کنترلر ندارد، که همانند گذشته به استفاده از متد [List<Creneau> getAllCreneaux(long idMedecin)] ادامه خواهد داد. آنچه تغییر خواهد کرد، پیادهسازی این متد در کلاس [ApplicationModel] است.
8.4.11.5. کلاس ایستا
کلاس [Static] حاوی مجموعهای از متدهای ابزار استاتیک است که هیچ جنبه «کسبوکاری» یا «وب» ندارند:
![]() |
کد آن به شرح زیر است:
package rdvmedecins.web.helpers;
import java.util.ArrayList;
import java.util.List;
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;
}
}
- خط ۱۲: متد [Static.getErreursForException]، که در متد [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()]، میشود.
8.4.11.6. اسکلت کنترلر [RdvMedecinsController]
![]() |
اکنون پردازش URL توسط سرویس وب را به تفصیل بررسی خواهیم کرد. سه کلاس اصلی در این پردازش دخیل هستند:
- کنترلکننده [RdvMedecinsController];
- کلاس متدهای ابزار [Static]؛
- کلاس کش [ApplicationModel]؛
![]() |
کنترلکننده [RdvMedecinsController] به شرح زیر است:
package rdvmedecins.web.controllers;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.web.helpers.Static;
import rdvmedecins.web.models.ApplicationModel;
import rdvmedecins.web.models.PostAjouterRv;
import rdvmedecins.web.models.PostSupprimerRv;
import rdvmedecins.web.models.Response;
@Controller
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
// فهرست پیامها
private List<String> messages;
// نقشهسازان jSON
@Autowired
private ObjectMapper jsonMapper;
@Autowired
private ObjectMapper jsonMapperShortCreneau;
@Autowired
private ObjectMapper jsonMapperLongRv;
@Autowired
private ObjectMapper jsonMapperShortRv;
@PostConstruct
public void init() {
// پیامهای خطای برنامه
messages = application.getMessages();
}
// فهرست پزشکان
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins() throws JsonProcessingException {...}
// فهرست مشتریان
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllClients() throws JsonProcessingException {...}
//فهرست نوبتهای پزشک
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {...}
//فهرست نوبتهای پزشک
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
throws JsonProcessingException {...}
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {...}
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getMedecinById(@PathVariable("id") long id) String origin) throws JsonProcessingException {...}
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {...}
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {...}
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {...}
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {...}
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
throws JsonProcessingException {...}
@RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String authenticate() throws JsonProcessingException {...}
}
- خط ۳۵: آناوتیشن [@Controller] کلاس [RdvMedecinsController] را به عنوان یک کنترلر Spring مشخص میکند، حرف «C» در MVC;
- خطوط ۳۸–۳۹: یک شیء از نوع [ApplicationModel] توسط Spring در اینجا تزریق خواهد شد. ما قبلاً این را معرفی کردهایم؛
- خطوط ۴۱–۴۲: یک شیء از نوع [RdvMedecinsCorsController] توسط Spring در اینجا تزریق خواهد شد. ما تنها بعداً این شیء را معرفی خواهیم کرد؛
- خطوط ۴۸–۵۸: مپکنندههای jSON تعریفشده در کلاس پیکربندی [WebConfig]؛
- خط ۶۰: آناوتیشن [@PostConstruct] متدی را مشخص میکند که بلافاصله پس از instantiate شدن کلاس اجرا شود. هنگامی که این متد اجرا میشود، اشیایی که توسط Spring تزریق شدهاند در دسترس هستند؛
- خط ۶۳: هرگونه پیام خطا از شیء [ApplicationModel] بازیابی میشود. این شیء هنگام راهاندازی برنامه ایجاد شد و تلاش کرد تا پزشکان و مشتریان را در حافظه پنهان ذخیره کند. اگر این کار با شکست مواجه شد، آنگاه [messages!=null] را داریم. این به متدهای کنترلکننده اجازه میدهد تا تشخیص دهند که آیا برنامه به درستی راهاندازی شده است یا خیر؛
- خطوط ۶۷–۱۱۸: متدهای URL که توسط سرویس [web / jSON] ارائه شدهاند. تمام متدها رشته jSON را از یک شی از نوع [Response<T>] به شرح زیر بازمیگردانند:
![]() |
package rdvmedecins.web.models;
import java.util.List;
public class Response<T> {
// ----------------- ویژگیها
//وضعیت عملیات
private int status;
// هرگونه پیام خطا
private List<String> messages;
// بدنه پاسخ
private T body;
// سازندهها
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// گیرندهها و تنظیمکنندهها
...
}
- خط ۹: یک کد خطا: ۰ به معنای عدم وجود خطا است؛
- خط ۱۱: اگر [status!=0] باشد، آنگاه [messages] یک لیست از پیامهای خطا است؛
- خط ۱۳: یک شیء T در پاسخ جاسازی شده است. در صورت بروز خطا، T روی null تنظیم میشود؛
این شیء قبل از ارسال به مرورگر مشتری به صورت jSON سریالیزه میشود؛
- خط ۶۷: URL نمایانشده، [/getAllMedecins] است. کلاینت باید برای ارسال درخواست خود از یک متد [GET] استفاده کند (متد = RequestMethod.GET). اگر این URL توسط یک POST درخواست میشد، رد میشد و Spring MVC یک کد خطای HTTP را به کلاینت وب ارسال میکرد. خود متد پاسخ را به کلاینت بازمیگرداند (خط ۶۸). این یک رشته خواهد بود (خط ۶۷). سربرگ HTTP [Content-type : application/json; charset=UTF-8] برای اطلاعرسانی به کلاینت ارسال میشود که در آستانه دریافت یک رشته jSON است (خط 67);
- خط ۷۷: URL توسط {idMedecin} تنظیم میشود. این پارامتر با استفاده از حاشیهنویسی [@PathVariable] در خط ۷۹ بازیابی میشود؛
- خط ۷۹: پارامتر [long idMedecin] مقدار خود را از پارامتر {idMedecin} از URL [@PathVariable("idMedecin")] میگیرد. پارامتر در URL و پارامتر در متد ممکن است نامهای متفاوتی داشته باشند. در اینجا باید توجه داشت که [@PathVariable("idMedecin")] از نوع String است (کل URL یک String است)، در حالی که پارامتر [long idMedecin] از نوع [long] است. تبدیل نوع بهطور خودکار انجام میشود. در صورت عدم موفقیت این تبدیل نوع، کد خطا HTTP بازگردانده میشود؛
- خط ۱۰۵: حاشیهنویسی [@RequestBody] به بدنه درخواست اشاره دارد. در یک درخواست GET تقریباً هرگز بدنه وجود ندارد (اگرچه میتوان آن را گنجاند). در یک درخواست POST معمولاً یک بدنه وجود دارد (اگرچه میتوان آن را نگنجاند). برای URL و [ajouterRv]، کلاینت وب رشته زیر را در POST خود ارسال میکند:
سینتکس [@RequestBody PostAjouterRv post] (خط ۱۰۵) ، در ترکیب با این واقعیت که متد در خط ۱۰۳ انتظار jSON [consumes = "application/json; charset=UTF-8"] را دارد، به این معنی است که رشته jSON ارسال شده توسط کلاینت وب به یک شی از نوع [PostAjouterRv] سریالیزه معکوس خواهد شد. این به شرح زیر است:
package rdvmedecins.web.models;
public class PostAjouterRv {
// دادههای پست
private String jour;
private long idClient;
private long idCreneau;
// گیرندهها و تنظیمکنندهها
...
}
در اینجا نیز، تبدیلهای نوع لازم بهطور خودکار انجام خواهد شد؛
- در خطوط ۱۰۷–۱۰۹، مکانیزم مشابهی برای URL و [/supprimerRv] وجود دارد. رشته jSON ارسالشده به شرح زیر است:
و نوع [PostSupprimerRv] به شرح زیر است:
package rdvmedecins.web.models;
public class PostSupprimerRv {
// دادههای پست
private long idRv;
// گیرندهها و تنظیمکنندهها
...
}
8.4.11.7. URL و [/getAllMedecins]
URL و [/getAllMedecins] توسط روش زیرِ کنترلکننده [RdvMedecinsController] پردازش میشوند:
// فهرست پزشکان
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins() throws JsonProcessingException {
// پاسخ
Response<List<Medecin>> response;
//وضعیت درخواست
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// فهرست پزشکان
try {
response = new Response<>(0, null, application.getAllMedecins());
} catch (RuntimeException e) {
response = new Response<>(1, Static.getErreursForException(e), null);
}
}
// پاسخ
return jsonMapper.writeValueAsString(response);
}
- خطوط ۹–۱۰: سیستم بررسی میکند که آیا برنامه به درستی راهاندازی شده است (messages==null). در غیر این صورت، پاسخی با status=-1 و body=messages بازگردانده میشود؛
- خط ۱۳: در غیر این صورت، فهرست پزشکان از کلاس [ApplicationModel] درخواست میشود؛
- خط ۱۹: رشته jSON از پاسخ با استفاده از مپر jSON [jsonMapper] ارسال میشود زیرا کلاس [Medecin]فیلتر jSON ندارد. پاسخ ممکن است بدون خطا باشد (خط 14) یا شامل خطا باشد (خط 16). متد [application.getAllMedecins()] استثنا پرتاب نمیکند زیرا صرفاً یک لیست کششده را بازمیگرداند. با این حال، ما این مدیریت استثنا را حفظ خواهیم کرد در صورتی که پزشکان دیگر در کش موجود نباشند؛
ما هنوز سناریویی را که در آن برنامه بهطور نادرست راهاندازی شده باشد، نشان ندادهایم. فرآیندهای SGBD و MySQL5 را متوقف کنیم، سرویس وب را راهاندازی کنیم و سپس فرآیندهای URL و [/getAllMedecins] را درخواست کنیم:

ما واقعاً با یک خطا مواجه میشویم. در شرایط عادی، نمای زیر را مشاهده میکردیم:
![]() |
8.4.11.8. URL [/getAllClients]
URL [/getAllClients] توسط متد زیر از کنترلر [RdvMedecinsController] پردازش میشود:
// فهرست مشتریان
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllClients() throws JsonProcessingException {
// پاسخ
Response<List<Client>> response;
//وضعیت درخواست
if (messages != null) {
response = new Response<>(-1, messages, null);
}
// فهرست مشتریان
try {
response = new Response<>(0, null, application.getAllClients());
} catch (RuntimeException e) {
response = new Response<>(1, Static.getErreursForException(e), null);
}
// پاسخ
return jsonMapper.writeValueAsString(response);
}
این مشابه روش [getAllMedecins] است که قبلاً مورد بحث قرار گرفت. نتایج حاصل به شرح زیر است:
![]() |
8.4.11.9. URL [/getAllCreneaux/{idMedecin}]
URL و [/getAllCreneaux/{idMedecin}] توسط روش زیرِ کنترلکننده [RdvMedecinsController] پردازش میشوند:
// فهرست شکافهای نوبتدهی پزشک
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {
// پاسخ
Response<List<Creneau>> response;
//وضعیت درخواست
if (messages != null) {
response = new Response<>(-1, messages, null);
}
// بازیابی پزشک
Response<Medecin> responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
} else {
Medecin médecin = responseMedecin.getBody();
// ساعات نوبتدهی پزشک
try {
response = new Response<>(0, null, application.getAllCreneaux(médecin.getId()));
} catch (RuntimeException e1) {
response = new Response<>(3, Static.getErreursForException(e1), null);
}
}
// پاسخ
return jsonMapperShortCreneau.writeValueAsString(response);
}
- خط ۱۲: پزشکی که توسط پارامتر [id] شناسایی شده است، از طریق یک متد محلی پرسوجو میشود:
private Response<Medecin> getMedecin(long id) {
// در حال بازیابی پزشک
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (RuntimeException e1) {
return new Response<Medecin>(1, Static.getErreursForException(e1), null);
}
//آیا دکتر وجود دارد؟
if (médecin == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le médecin d'id [%s] n'existe pas", id));
return new Response<Medecin>(2, messages, null);
}
// باشه
return new Response<Medecin>(0, null, médecin);
}
ما از این متد با یک status در [0,1,2] بازمیگردیم. بیایید به کد متد [getAllCreneaux] بازگردیم:
- خطوط ۱۳–۱۴: اگر status!=0 باشد، پاسخی با خطا ساخته میشود؛
- خط ۱۶: پزشک بازیابی میشود؛
- خط ۱۹: بازخوانی شکافهای قرار ملاقات این پزشک؛
- خط ۲۵: یک شیء [List<Creneau>] بهعنوان پاسخ ارسال میشود. بیایید تعریف کلاس [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;
...
}
- خط ۱۳: پزشک در حالت [FetchType.LAZY] جستجو میشود؛
بیایید پرسوجوی JPQL را که متد [getAllCreneaux] را در لایه [DAO] پیادهسازی میکند، به یاد آوریم:
@Query("select c from Creneau c where c.medecin.id=?1")
علامت [c.medecin.id] باعث پیوستن جداول [CRENEAUX] و [MEDECINS] میشود. در نتیجه، پرسوجو تمام شکافهای قرار ملاقات پزشک را بازمیگرداند و نام پزشک در هر یک درج شده است. وقتی این اسلاتها در jSON سریال میشوند، رشتهٔ نام دکتر jSON در هر یک از آنها ظاهر میشود. این غیرضروری است. برای کنترل سریالسازی، به دو چیز نیاز داریم:
- دسترسی به شیء در حال سریالیسازی؛
- پیکربندی شیء مورد نظر برای سریالسازی؛
نقطه ۱ با تزریق مبدل مناسب jSON برای شیء به کنترلر تأیید میشود:
@Autowired
private ObjectMapper jsonMapperShortCreneau;
نقطه ۲ با افزودن یک انوتیشن به کلاس [Creneau] که در پروژه [rdvmedecins-metier-dao] تعریف شده است، حاصل میشود:
![]() |
@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
public class Creneau extends AbstractEntity {
...
- خط ۳: یک حاشیهنویسی از کتابخانه جکسون jSON. این یک فیلتر به نام [creneauFilter] ایجاد میکند. با استفاده از این فیلتر، ما قادر خواهیم بود به صورت برنامهای تعریف کنیم که کدام فیلدها باید سریال شوند یا نباید؛
سریالسازی شیء [Creneau] در خط زیر از متد [getAllCreneaux] انجام میشود:
// پاسخ
return jsonMapperShortCreneau.writeValueAsString(response);
نقشهبردار jSON [jsonMapperShortCreneau] در کلاس [WebConfig] به صورت زیر تعریف شده است:
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
- خط ۵: فیلتر با نام [creneauFilter] با فیلتر [creneauFilter] از خط ۴ مرتبط است. این فیلتر شیء [Creneau] را بدون فیلد [medecin] سریالیزه میکند؛
نتیجهای که توسط متد [getAllCreneaux] بازگردانده میشود، رشته jSON از نوع [Response<List<Creneau>] است.
نتایج بهدستآمده به شرح زیر است:
![]() |
یا در صورتی که بازه زمانی وجود نداشته باشد:
![]() |
از این مثال میتوانیم قاعده زیر را استخراج کنیم:
- متدهای وب سرور / jSON یک شیء از نوع [Response<T>] را بازمیگردانند، که به jSON سریالیزه میشود؛
- اگر نوع T یک یا چند فیلتر jSON داشته باشد، از یک مپر با همین فیلترها برای سریالیسازی آن استفاده خواهد شد؛
8.4.11.10. URL [/getRvMedecinJour/{idMedecin}/{jour}]
URL [/getRvMedecinJour/{idMedecin}/{jour}] توسط متد زیر از کنترلر [RdvMedecinsController] پردازش میشود:
// فهرست نوبتهای پزشک
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin)
throws JsonProcessingException {
// پاسخ
Response<List<Rv>> response=null;
boolean erreur = false;
//وضعیت درخواست
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
//بررسی تاریخ
Date jourAgenda = null;
if (!erreur) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<List<Rv>>(3, messages, null);
erreur = true;
}
}
Response<Medecin> responseMedecin = null;
if (!erreur) {
//بازیابی پزشک
responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
erreur = true;
}
}
if (!erreur) {
Medecin médecin = responseMedecin.getBody();
// فهرست قرار ملاقاتهایشان
try {
response = new Response<>(0, null, application.getRvMedecinJour(médecin.getId(), jourAgenda));
} catch (RuntimeException e1) {
response = new Response<>(4, Static.getErreursForException(e1), null);
}
}
// پاسخ
return jsonMapperLongRv.writeValueAsString(response);
}
- رشته jSON باید به نوع [Response<List<Rv>>] تبدیل شود. کلاس [Rv] دارای یک فیلد [Rv.creneau] است. اگر این فیلد سریال شود، فیلتر jSON [creneauFilter] با آن مواجه خواهد شد؛
- خط ۴۷: شیء از نوع [Response<List<Rv>>] از خط ۷ به jSON سریال میشود؛
بیایید موردی را در نظر بگیریم که فهرست قرارها در خط ۴۲ بهدست آمده است. کلاس [Rv] در پروژه [rdvmedecins-metier-dao] به شرح زیر تعریف شده است:
@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;
...
}
- خط ۱۱: مشتری با استفاده از حالت [FetchType.LAZY] جستجو میشود؛
- خط ۱۸: بازه زمانی با استفاده از حالت [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 ظاهر خواهد شد. با این حال، این اطلاعات تکراری نیز غیرضروری است. ما دیدیم که چگونه با استفاده از یک فیلتر jSON بر روی شیء [Creneau] این مشکل را حل کنیم. به دلیل حالتهای [FetchType.LAZY] فیلدهای [client] و [creneau] در کلاس [Rv]، بهزودی متوجه نیاز به اعمال یک فیلتر jSON بر روی کلاس [RV] در پروژه [rdvmedecins-metier-dao] خواهیم شد:
@Entity
@Table(name = "rv")
@JsonFilter("rvFilter")
public class Rv extends AbstractEntity {
...
ما سریالیسازی شیء [Rv] را با استفاده از فیلتر [rvFilter] بررسی خواهیم کرد. به نظر میرسد که در اینجا نیازی به اعمال فیلتر نیست، زیرا ما به تمام فیلدهای شیء [Rv] نیاز داریم. با این حال، از آنجا که مشخص کردهایم که کلاس دارای فیلتر jSON است، باید این فیلتر را برای هر سریالیزاسیون یک شی از نوع [Rv] تعریف کنیم؛ در غیر این صورت، یک استثنا پرتاب خواهد شد. برای این کار، از mapper زیر jSON که در کلاس [rdvMedecinsController] تعریف شده است، استفاده میکنیم:
@Autowired
private ObjectMapper jsonMapperLongRv;
این مپر در کلاس پیکربندی [WebConfig] به صورت زیر تعریف شده است:
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",creneauFilter));
return jsonMapperLongRv;
}
- خط ۴: ما مشخص میکنیم که تمام فیلدهای شیء [Rv] باید سریال شوند؛
- خط ۵: مشخص میکنیم که در شیء [Creneau]، فیلد [medecin] نباید سریال شود؛
- خط ۶: ما دو فیلتر [rvFilter] و [creneauFilter] را به فیلترهای jSON در شیء [jsonMapperLongRv] اضافه میکنیم؛
نتایج بهدستآمده به شرح زیر است:
![]() |
یا اینها، با یک روز بدون قرار ملاقات:
![]() |
یا این موارد، با روز نادرست:
![]() |
یا اینها که پزشک نادرستی دارند:
![]() |
8.4.11.11. URL و [/getAgendaMedecinJour/{idMedecin}/{jour}]
URL و [/getAgendaMedecinJour/{idMedecin}/{jour}] توسط کنترلر [RdvMedecinsController] با روش زیر پردازش میشوند:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin)
throws JsonProcessingException {
// پاسخ
Response<AgendaMedecinJour> response = null;
boolean erreur = false;
//وضعیت برنامه
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// تاریخ بررسی میشود
Date jourAgenda = null;
if (!erreur) {
//بررسی تاریخ
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
erreur = true;
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<>(3, messages, null);
}
}
// بازیابی پزشک
Medecin médecin = null;
if (!erreur) {
// در حال بازیابی دکتر
Response<Medecin> responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
} else {
médecin = responseMedecin.getBody();
}
}
// در حال بازیابی دفترچه خاطرات آنها
if (!erreur) {
try {
response = new Response<>(0, null, application.getAgendaMedecinJour(médecin.getId(), jourAgenda));
} catch (RuntimeException e1) {
erreur = true;
response = new Response<>(4, Static.getErreursForException(e1), null);
}
}
// پاسخ
return jsonMapperLongRv.writeValueAsString(response);
}
- سطور ۶، ۴۹: رشته jSON به صورت یک نوع [AgendaMedecinJour] در بر گرفته شده در یک شیء [Response] رندر میشود؛
نوع [AgendaMedecinJour] به شرح زیر است:
public class AgendaMedecinJour implements Serializable {
// fields
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
نوع [CreneauMedecinJour] به شرح زیر است:
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// fields
private Creneau creneau;
private Rv rv;
فیلدهای [creneau] و [rv] دارای فیلترهای jSON هستند که باید پیکربندی شوند. این کاری است که خط ۴۹ متد [getAgendaMedecinJour] انجام میدهد، که از مپرهای jSON و [jsonMapperLongRv] استفاده میکند، که قبلاً با آنها مواجه شدهایم:
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(
new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter", creneauFilter));
return jsonMapperLongRv;
}
نتایج بهدستآمده به شرح زیر است:
![]() |
در بالا میبینیم که در تاریخ 28/01/2015، دکتر PELISSIER در ساعت 8:20 صبح قرار ملاقاتی با خانم Brigitte BISTROU دارد؛
یا اینها، اگر تاریخ نادرست باشد:
![]() |
یا اینها اگر شماره دکتر نامعتبر باشد:
![]() |
8.4.11.12. URL [/getMedecinById/{id}]
URL و [/getMedecinById/{id}] توسط اعتبارسنج [RdvMedecinsController] با روش زیر پردازش میشوند:
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getMedecinById(@PathVariable("id") long id) throws JsonProcessingException {
//پاسخ
Response<Medecin> response;
//وضعیت برنامه
if (messages != null) {
response = new Response<Medecin>(-1, messages, null);
} else {
response = getMedecin(id);
}
// پاسخ
return jsonMapper.writeValueAsString(response);
}
- خطوط ۵، ۱۳: متد رشته jSON از نوع [Medecin] را برمیگرداند. این نوع هیچ anotation فیلتر jSON ندارد. بنابراین، در خط ۱۴، مپر jSON بدون فیلترها استفاده میشود؛
در خط ۱۰، متد [getMedecin] به شرح زیر است:
private Response<Medecin> getMedecin(long id) {
// بازیابی پزشک
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (RuntimeException e1) {
return new Response<Medecin>(1, Static.getErreursForException(e1), null);
}
// آیا دکتر وجود دارد؟
if (médecin == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le médecin d'id [%s] n'existe pas", id));
return new Response<Medecin>(2, messages, null);
}
// باشه
return new Response<Medecin>(0, null, médecin);
}
نتایج بهدستآمده به شرح زیر است:
![]() |
یا اینها اگر شمارهٔ دکتر نادرست باشد:
![]() |
8.4.11.13. URL [/getClientById/{id}]
URL [/getClientById/{id}] توسط کنترلر [RdvMedecinsController] با روش زیر پردازش میشود:
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {
// پاسخ
Response<Client> response;
//وضعیت درخواست
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
response = getClient(id);
}
// پاسخ
return jsonMapper.writeValueAsString(response);
}
- خطوط 5، 13: متد رشته jSON از نوع [Client] را برمیگرداند. این نوع هیچ anotation فیلتر jSON ندارد. بنابراین، در خط ۱۳، مپر jSON بدون فیلترها استفاده میشود؛
در خط ۱۱، متد [getClient] به شرح زیر است:
private Response<Client> getClient(long id) {
// بازیابی مشتری
Client client = null;
try {
client = application.getClientById(id);
} catch (RuntimeException e1) {
return new Response<Client>(1, Static.getErreursForException(e1), null);
}
//آیا کلاینت وجود دارد؟
if (client == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le client d'id [%s] n'existe pas", id));
return new Response<Client>(2, messages, null);
}
// OK
return new Response<Client>(0, null, client);
}
نتایج بهدستآمده به شرح زیر است:
![]() |
یا اینها اگر شماره مشتری نادرست باشد:
![]() |
8.4.11.14. URL [/getCreneauById/{id}]
URL و [/getCreneauById/{id}] توسط روش زیر در کنترلکننده [RdvMedecinsController] پردازش میشوند:
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {
// پاسخ
Response<Creneau> response;
//وضعیت درخواست
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// بازگرداندن بازه زمانی
response = getCreneau(id);
}
// پاسخ
return jsonMapperShortCreneau.writeValueAsString(response);
}
- خطوط ۵ و ۱۴: متد رشته jSON از نوع [Response<Creneau>] را برمیگرداند؛
خط ۸: متد [getCreneau] به شرح زیر است:
private Response<Creneau> getCreneau(long id) {
// بازیابی اسلات
Creneau créneau = null;
try {
créneau = application.getCreneauById(id);
} catch (RuntimeException e1) {
return new Response<Creneau>(1, Static.getErreursForException(e1), null);
}
//آیا اسلات وجود دارد؟
if (créneau == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le créneau d'id [%s] n'existe pas", id));
return new Response<Creneau>(2, messages, null);
}
// OK
return new Response<Creneau>(0, null, créneau);
}
بیایید کد موجودیت [Creneau] را به یاد بیاوریم:
@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
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;
- خطوط ۱۴–۱۶: از آنجا که فیلد [medecin] در حالت [fetch = FetchType.LAZY] است، هنگام بازیابی یک اسلات از طریق [id] آن، بازیابی نمیشود. بنابراین لازم است آن را از سریالیسازی مستثنی کرد. بدون این استثنا، یک خطای استثنا (exception) پرتاب میشود. این به آن دلیل است که شیء سریالیسازی [mapper] متد [getMedecin] را برای بازیابی فیلد [medecin] فراخوانی خواهد کرد. با این حال، در پیادهسازی JPA / Hibernate، حالت [fetch = FetchType.LAZY] از فیلد [medecin] یک شیء [Creneau] را بازگردانده است، که متد [getMedecin] آن برای بازیابی دکتر از QZXW2HTMLP004841 برنامهریزی شده استZQX. این به عنوان یک شیء [proxy] شناخته میشود. اکنون، بیایید معماری برنامه وب را به یاد بیاوریم:
![]() |
کنترلر در بلوک [Contrôleurs / Actions] قرار دارد. هنگامی که در این بلوک قرار گرفت، مفهوم زمینه JPA دیگر کاربرد ندارد. این زمینه در حین انجام عملیات در لایه [DAO] ایجاد میشود. این زمینه فراتر از آن نقطه پایدار نمیماند. بنابراین، هنگامی که کنترلر سعی در دسترسی به زمینه JPA دارد، یک استثنا پرتاب میشود که نشان میدهد این زمینه بسته شده است. برای جلوگیری از این استثنا، باید از سریالیسازی فیلد [medecin] از کلاس [Rv] جلوگیری شود. این کاری است که مپر jSON [jsonMapperShortCreneau] انجام میدهد:
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
نتایج بهدستآمده به شرح زیر است:
![]() |
یا اینها اگر شماره اسلات نادرست باشد:
![]() |
8.4.11.15. URL [/getRvById/{id}]
URL و [/getRvById/{id}] توسط روش زیر در کنترلکننده [RdvMedecinsController] پردازش میشوند:
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {
// پاسخ
Response<Rv> response;
//وضعیت درخواست
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
//بازیابی قرار ملاقات
response = getRv(id);
}
// پاسخ
return jsonMapperShortRv.writeValueAsString(response);
}
- خطوط 5 و 14: این متد رشته jSON از نوع [Response<Rv>] را برمیگرداند؛
خط ۱۱: متد [getRv] به شرح زیر است:
private Response<Rv> getRv(long id) {
// بازیابی Rv
Rv rv = null;
try {
rv = application.getRvById(id);
} catch (RuntimeException e1) {
return new Response<Rv>(1, Static.getErreursForException(e1), null);
}
//آیا Rv وجود دارد؟
if (rv == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le rendez-vous d'id [%s] n'existe pas", id));
return new Response<Rv>(2, messages, null);
}
// OK
return new Response<Rv>(0, null, rv);
}
کلاس [Rv] دارای دو فیلد با آنوتیشن [fetch = FetchType.LAZY] است: فیلدهای [creneau] و [client]. بنابراین این فیلدها هنگام بازیابی یک [Rv] از طریق کلید اصلی آن، بازیابی نمیشوند. به همان دلایلی که قبلاً گفته شد، بنابراین باید از سریالیسازی مستثنی شوند. این کاری است که مپر [jsonMapperShortRv] زیر، که در کلاس [WebConfig] تعریف شده است، انجام میدهد:
@Bean
public ObjectMapper jsonMapperShortRv() {
ObjectMapper jsonMapperShortRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
return jsonMapperShortRv;
}
نتایج بهدستآمده به شرح زیر است:
![]() |
یا در صورت نادرست بودن شماره نوبت:
![]() |
8.4.11.16. URL [/ajouterRv]
URL و [/ajouterRv] توسط روش زیر در کنترلکننده [RdvMedecinsController] پردازش میشوند:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {
//پاسخ
Response<Rv> response = null;
boolean erreur = false;
//وضعیت برنامه
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
//بازیابی مقادیر ارسالشده
String jour;
long idCreneau = -1;
long idClient = -1;
Date jourAgenda = null;
if (!erreur) {
// بازیابی مقادیر ارسالشده
jour = post.getJour();
idCreneau = post.getIdCreneau();
idClient = post.getIdClient();
//تاریخ را بررسی کنید
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<>(6, messages, null);
erreur = true;
}
}
// بازیابی شکاف زمانی
Response<Creneau> responseCréneau = null;
if (!erreur) {
//بازیابی بازه زمانی
responseCréneau = getCreneau(idCreneau);
if (responseCréneau.getStatus() != 0) {
erreur = true;
response = new Response<>(responseCréneau.getStatus(), responseCréneau.getMessages(), null);
}
}
//بازیابی مشتری
Response<Client> responseClient = null;
Creneau créneau = null;
if (!erreur) {
créneau = (Creneau) responseCréneau.getBody();
//بازیابی مشتری
responseClient = getClient(idClient);
if (responseClient.getStatus() != 0) {
erreur = true;
response = new Response<>(responseClient.getStatus() + 2, responseClient.getMessages(), null);
}
}
if (!erreur) {
Client client = responseClient.getBody();
// افزودن قرار ملاقات
try {
response = new Response<>(0, null, application.ajouterRv(jourAgenda, créneau, client));
} catch (RuntimeException e1) {
erreur = true;
response = new Response<>(5, Static.getErreursForException(e1), null);
}
}
// پاسخ
return jsonMapperLongRv.writeValueAsString(response);
}
- خطوط ۵، ۶۷: متد باید رشته jSON از نوع [Response<Rv>] را بازگرداند؛
- خط ۳: حاشیهنویسی [@RequestBody PostAjouterRv post] بدنه POST را بازیابی کرده و آن را در پارامتر [PostAjouterRv post] قرار میدهد. این محتوا از jSON [consumes = "application/json; charset=UTF-8"] است که بهطور خودکار به نوع زیر [PostAjouterRv] سریالیسازی میشود:
public class PostAjouterRv {
// دادههای ارسالشده
private String jour;
private long idClient;
private long idCreneau;
...
- سپس کدی وجود دارد که به شکلی یا دیگری قبلاً با آن مواجه شده است؛
- خط ۶۷: تنظیم فیلترهای jSON، [creneauFilter] و [rvFilter]. این متد رشته jSON را به نوع [Response<Rv>] تبدیل میکند، جایی که Rv در خط 61 به دست آمده است. شیء [Rv] یک شیء [Creneau] و همچنین یک شیء [Client] را در بر میگیرد. شیء [Creneau] وابستگی [FetchType.LAZY] به شیء [Medecin] دارد و در خطوط 36–44 بازیابی شد. این شیء از طریق کلید اصلیاش از زمینه JPA بازیابی شد و بدون وابستگیاش [FetchType.LAZY] به دست آمد. در نهایت،
- شیء [Rv] تمام وابستگیهای خود را دارد. این وابستگیها قابل سریالسازی هستند؛
- شیء [Creneau] وابستگی خود [medecin] را ندارد. این بدان معناست که مورد دوم نباید سریال شود؛
نقشهبردار jSON [jsonMapperLongRv] که در کلاس [WebConfig] تعریف شده است، این محدودیتها را برآورده میکند:
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",creneauFilter));
return jsonMapperLongRv;
}
نتایج بهدستآمده با کلاینت [Advanced Rest Client] به این صورت است:
![]() |
- در [1]، URL از POST;
- [2] به POST تبدیل میشود؛
- در [3]، مقدار ارسالشده؛
- در [4a]، این مقدار ارسالشده از jSON است؛
![]() |
- در [4b]، کلاینت نشان میدهد که در حال ارسال jSON است؛
- در [5]، سرور نشان میدهد که jSON را بازمیگرداند؛
![]() |
- در [6]، پاسخ سرور jSON است که نمایانگر قرار ملاقات افزودهشده میباشد. این شناسه [id] را برای قرار ملاقات افزودهشده نشان میدهد؛
خروجی زیر با یک شماره اسلات ناموجود به دست میآید:
![]() |
8.4.11.17. URL و [/supprimerRv]
URL و [/supprimerRv] توسط متد زیر از کنترلر [RdvMedecinsController] پردازش میشوند:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {
// پاسخ
Response<Void> response = null;
boolean erreur = false;
// سربرگها CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
//وضعیت برنامه
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// بازیابی مقادیر ارسالشده
long idRv = post.getIdRv();
// بازیابی RV
if (!erreur) {
Response<Rv> responseRv = getRv(idRv);
if (responseRv.getStatus() != 0) {
response = new Response<>(responseRv.getStatus(), responseRv.getMessages(), null);
erreur = true;
}
}
if (!erreur) {
// حذف rv
try {
application.supprimerRv(idRv);
response = new Response<Void>(0, null, null);
} catch (RuntimeException e1) {
response = new Response<>(3, Static.getErreursForException(e1), null);
}
}
// پاسخ
return jsonMapper.writeValueAsString(response);
}
- خط ۵: نوع [Void] کلاسی است که با نوع ابتدایی [void] مطابقت دارد؛
- خطوط ۵ و ۳۴: متد رشته jSON از نوع [Response<Void>] را بازمیگرداند که هیچ فیلتر jSON ندارد. بنابراین، در خط ۳۴، مپر jSON بدون فیلترها استفاده میشود؛
- خط ۳: متد بدنه POST را بهعنوان پارامتر میگیرد، یعنی مقدار ارسالشده. این در قالب jSON [consumes = "application/json; charset=UTF-8"] دریافت شده و بهطور خودکار به نوع زیر [PostSupprimerRv] سریالبرداری میشود:
public class PostSupprimerRv {
// ارسال دادهها
private long idRv;
- خط ۲۸: هنگامی که حذف با موفقیت انجام شود، پاسخی با [status=0] ارسال میشود؛
نتایج بهدستآمده به شرح زیر است:
![]() |
![]() |
- در [5]، فیلد [status=0] نشان میدهد که حذف با موفقیت انجام شده است؛
با یک شماره قرار ملاقات ناموجود، نتیجه زیر به دست میآید:
![]() |
ما کار با کنترلر را به پایان رساندیم. اکنون میبینیم چگونه پروژه را اجرا کنیم.
8.4.11.18. کلاس قابل اجرای سرویس وب
![]() |
کلاس [Boot] [1] به شرح زیر است:
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);
}
}
در خط ۱۰، متد استاتیک [SpringApplication.run] با کلاس پیکربندی پروژه [AppConfig] بهعنوان اولین پارامتر فراخوانی میشود. این متد پروژه را بهطور خودکار پیکربندی میکند، سرور Tomcat جاسازیشده در وابستگیها را راهاندازی میکند و کنترلر [RdvMedecinsController] را روی آن مستقر میسازد.
لاگها توسط فایلهای زیر مدیریت میشوند: [2]:
[logback.xml]
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- کدگذارها بهطور پیشفرض نوع ch.qos.logback.classic.encoder.PatternLayoutEncoder را دریافت میکنند -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- کنترل سطح لاگ -->
<root level="info"> <!--خاموش، اطلاعات، اشکالزدایی، هشدار -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- خط ۹: سطح عمومی لاگ روی [info] تنظیم شده است؛
[application.properties]
logging.level.org.springframework.web=INFO
logging.level.org.hibernate=OFF
spring.main.show-banner=false
خطوط ۱–۲ سطح لاگگیری مشخصی را برای بخشهای خاصی از برنامه فعال میکنند:
- خط ۱: ما لاگها را از لایه [web] میخواهیم؛
- خط ۲: ما لاگهایی از لایه [JPA] را نمیخواهیم؛
- خط ۳: بدون بنر Spring Boot؛
لاگهای تولیدشده در زمان اجرا به شرح زیر است:
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs multiple times on the classpath.
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-metier-dao/target/classes/logback.xml]
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:06:04,342 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - debug attribute not set
11:06:04,342 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender]
11:06:04,342 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [STDOUT]
11:06:04,357 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property
11:06:04,404 |-INFO in ch.qos.logback.classic.joran.action.RootLoggerAction - Setting level of ROOT logger to INFO
11:06:04,404 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [STDOUT] to Logger[ROOT]
11:06:04,404 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.
11:06:04,420 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@56f4468b - Registering current configuration as safe fallback point
11:06:04.732 [main] INFO rdvmedecins.web.boot.Boot - Starting Boot on Gportpers3 with PID 420 (D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server\target\classes started by usrlocal in D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server)
11:06:04.775 [main] INFO o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:06:04 CEST 2015]; root of context hierarchy
11:06:05.538 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http)
11:06:05.688 [main] INFO o.a.catalina.core.StandardService - Starting service Tomcat
11:06:05.689 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet Engine: Apache Tomcat/8.0.26
11:06:05.833 [localhost-startStop-1] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
11:06:05.833 [localhost-startStop-1] INFO o.s.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 1061 ms
11:06:06.231 [localhost-startStop-1] INFO o.s.o.j.LocalContainerEntityManagerFactoryBean - Building JPA container EntityManagerFactory for persistence unit 'default'
11:06:09.234 [localhost-startStop-1] INFO o.s.s.web.DefaultSecurityFilterChain - Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@12d14fa, org.springframework.security.web.context.SecurityContextPersistenceFilter@29823fb6, org.springframework.security.web.header.HeaderWriterFilter@662d93b2, org.springframework.security.web.authentication.logout.LogoutFilter@2d81ee0, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@52aa47ad, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@60bd7a74, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5a374232, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@7ddb4452, org.springframework.security.web.session.SessionManagementFilter@2cd9855f, org.springframework.security.web.access.ExceptionTranslationFilter@2263f0a2, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@192ce7f6]
11:06:09.255 [localhost-startStop-1] INFO o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/*]
11:06:09.255 [localhost-startStop-1] INFO o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'springSecurityFilterChain' to: [/*]
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/authenticate],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.Void> rdvmedecins.web.controllers.RdvMedecinsController.authenticate(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAgendaMedecinJour/{idMedecin}/{jour}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllCreneaux/{idMedecin}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getAllCreneaux(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvMedecinJour/{idMedecin}/{jour}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getRvMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getMedecinById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<rdvmedecins.entities.Medecin> rdvmedecins.web.controllers.RdvMedecinsController.getMedecinById(long,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getClientById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<rdvmedecins.entities.Client> rdvmedecins.web.controllers.RdvMedecinsController.getClientById(long,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/supprimerRv],methods=[POST],consumes=[application/json;charset=UTF-8]}" onto public rdvmedecins.web.models.Response<java.lang.Void> rdvmedecins.web.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.web.models.PostSupprimerRv,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllClients],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.util.List<rdvmedecins.entities.Client>> rdvmedecins.web.controllers.RdvMedecinsController.getAllClients(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/ajouterRv],methods=[POST],consumes=[application/json;charset=UTF-8]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.web.models.PostAjouterRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getCreneauById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getCreneauById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllMedecins],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.util.List<rdvmedecins.entities.Medecin>> rdvmedecins.web.controllers.RdvMedecinsController.getAllMedecins(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getRvById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
...
11:06:09.677 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:06:04 CEST 2015]; root of context hierarchy
11:06:09.770 [main] INFO o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8080"]
11:06:09.786 [main] INFO o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
11:06:09.802 [main] INFO o.a.tomcat.util.net.NioSelectorPool - Using a shared selector for servlet write/read
11:06:09.817 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
11:06:09.817 [main] INFO rdvmedecins.web.boot.Boot - Started Boot in 5.319 seconds (JVM running for 6.053)
- خط ۱۸: سرور Tomcat فعال است؛
- خط ۲۱: زمینه Spring در حال инициализация است؛
- خطوط 27–38: منابع URL که توسط سرویس وب ارائه شدهاند، کشف شدهاند؛
- خط ۴۴: سرور Tomcat آماده است و در حال انتظار برای درخواستها روی پورت ۸۰۸۰ است؛
اگر فایل [application.properties] را به صورت زیر تغییر دهیم:
logging.level.org.springframework.web: OFF
logging.level.org.hibernate:OFF
spring.main.show-banner=false
لاگهای زیر تولید میشوند:
علاوه بر این، اگر فایل [logback.xml] را به صورت زیر تغییر دهیم:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- نوع رمزگذارها بهطور پیشفرض تعیین میشود ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!--کنترل سطح لاگ -->
<root level="off"> <!--خاموش، اطلاعات، اشکالزدایی، هشدار -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
لاگهای زیر تولید میشوند:
بنابراین میتوانیم ببینیم که ما تا حدی بر روی لاگهایی که در کنسول ظاهر میشوند کنترل داریم. سطح [info] اغلب سطح لاگ صحیح است.
اکنون یک سرویس وب کاربردی داریم که میتوان با استفاده از یک کلاینت وب به آن پرسوجو کرد. اکنون به امنیت این سرویس میپردازیم: میخواهیم تنها افراد خاصی بتوانند قرارهای ملاقات پزشکان را مدیریت کنند. برای این کار از چارچوب Spring Security، بخشی از اکوسیستم Spring، استفاده خواهیم کرد.
8.4.12. مقدمهای بر Spring Security
ما بار دیگر با دنبال کردن مراحل ۱ تا ۳ زیر، یک راهنمای Spring را وارد میکنیم:
![]() |
![]() |
پروژه از عناصر زیر تشکیل شده است:
- در پوشه [templates]، صفحات HTML پروژه را مییابیم؛
- [Application]: کلاس اجرایی پروژه است؛
- [MvcConfig]: کلاس پیکربندی Spring MVC است؛
- [WebSecurityConfig]: کلاس پیکربندی Spring Security است؛
8.4.12.1. پیکربندی Maven
پروژه [3] یک پروژه 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-securing-web</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.10.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>
<!-- end::security[] -->
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- خطوط ۱۰–۱۴: پروژه یک پروژه Spring Boot است؛
- خطوط 17–20: وابستگی به فریمورک [Thymeleaf]؛
- خطوط 22–25: وابستگی به فریمورک Spring Security؛
8.4.12.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:href="@{/hello}"] ویژگی [href] را برای تگ تولید خواهد کرد. مقدار [@{/hello}] مسیر [<context>/hello] را تولید میکند، که در آن [context] زمینهٔ برنامهٔ وب است؛
کد تولیدشده HTML به شرح زیر است:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click
<a href="/hello">here</a>
to see a greeting.
</p>
</body>
</html>
نما [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}]]!
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
</body>
</html>
- خط ۹: ویژگی [th:inline="text"] متن تگ را تولید میکند. این متن شامل یک عبارت $ است که باید ارزیابی شود. عنصر [[${#httpServletRequest.remoteUser}]] مقدار ویژگی [RemoteUser] از پرسوجوی جاری HTTP است. این نام کاربر واردشده است؛
- خط ۱۰: یک فرم HTML. ویژگی [th:action="@{/logout}"] ویژگی [action] از تگ [form] را تولید خواهد کرد. مقدار [@{/logout}] مسیر [<context>/logout] را تولید خواهد کرد، که در آن [context] زمینهٔ برنامهٔ وب است؛
کد تولیدشده HTML به شرح زیر است:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1>Hello user!</h1>
<form method="post" action="/logout">
<input type="submit" value="Sign Out" />
<input type="hidden" name="_csrf" value="b152e5b9-d1a4-4492-b89d-b733fe521c91" />
</form>
</body>
</html>
- خط ۸: ترجمه Hello [[${#httpServletRequest.remoteUser}]]!;
- خط ۹: ترجمه @{/logout};
- خط ۱۱: یک فیلد مخفی با نام (ویژگی 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>
- خط ۱۰: ویژگی [th:if="${param.error}"] تضمین میکند که تگ تنها در صورتی تولید میشود که URL، که صفحه ورود را نمایش میدهد، شامل پارامتر [error] (http://context/login?error) باشد؛
- خط ۱۰: ویژگی [th:if="${param.logout}"] تضمین میکند که تگ تنها در صورتی تولید میشود که URL، که صفحه ورود را نمایش میدهد، شامل پارامتر [logout] (http://context/login?logout) باشد؛
- خطوط ۱۱–۲۳: یک فرم HTML؛
- خط ۱۱: فرم به URL [<context>/login] ارسال خواهد شد، که در آن <context> زمینهٔ وباپلیکیشن است؛
- خط ۱۳: یک فیلد ورودی به نام [username];
- خط ۱۷: یک فیلد ورودی با نام [password];
کد تولیدشده HTML به شرح زیر است:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div>
You have been logged out.
</div>
<form method="post" action="/login">
<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>
<input type="hidden" name="_csrf" value="ef809b0a-88b4-4db9-bc53-342216b77632" />
</form>
</body>
</html>
توجه داشته باشید که در خط ۲۸، Thymeleaf یک فیلد مخفی به نام [_csrf] اضافه کرده است.
8.4.12.3. پیکربندی بهار 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");
}
}
- خط ۷: آناوتیشن [@Configuration] کلاس [MvcConfig] را بهعنوان یک کلاس پیکربندی تعیین میکند؛
- خط ۸: کلاس [MvcConfig] از کلاس [WebMvcConfigurerAdapter] ارث میبرد تا برخی متدها را بازنویسی کند؛
- خط ۱۰: بازتعریف یک متد از کلاس والد؛
- خطوط ۱۱–۱۶: متد [addViewControllers] به URL اجازه میدهد تا با ویوهای HTML مرتبط شود. ارتباطات زیر برقرار میشوند:
URL | نما |
/templates/home.html | |
/templates/hello.html | |
/templates/login.html |
پسوند [html] و پوشه [templates] مقادیر پیشفرض مورد استفاده توسط Thymeleaf هستند. آنها را میتوان از طریق پیکربندی تغییر داد. پوشه [templates] باید در ریشه classpath پروژه قرار داشته باشد:
![]() |
در [1]، پوشههای [java] و [resources] هر دو پوشههای منبع هستند. این بدان معناست که محتویات آنها در ریشهٔ مسیر کلاس پروژه قرار خواهد گرفت. بنابراین، در [2]، پوشههای [hello] و [templates] در ریشهٔ مسیر کلاس پروژه قرار خواهند گرفت.
8.4.12.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");
}
}
- خط ۹: آناوتیشن [@Configuration] کلاس [WebSecurityConfig] را بهعنوان یک کلاس پیکربندی تعیین میکند؛
- خط ۱۰: anotation [@EnableWebSecurity] کلاس [WebSecurityConfig] را بهعنوان یک کلاس پیکربندی Spring Security تعیین میکند؛
- خط ۱۱: کلاس [WebSecurity] کلاس [WebSecurityConfigurerAdapter] را برای بازنویسی برخی متدها گسترش میدهد؛
- خط ۱۲: یک متد از کلاس والد بازنویسی شده است؛
- خطوط ۱۳–۱۶: متد [configure(HttpSecurity http)] برای تعریف حقوق دسترسی به کلاسهای مختلف URL در برنامه، بازتعریف شده است؛
- خط ۱۴: متد [http.authorizeRequests()] اجازه میدهد URLها با حقوق دسترسی مرتبط شوند. ارتباطات زیر در اینجا برقرار میشوند:
URL | قاعده | کد |
دسترسی بدون احراز هویت | | |
دسترسی فقط با احراز هویت |
- خط ۱۵: روش احراز هویت را تعریف میکند. احراز هویت از طریق فرمهای در دسترس همگان انجام میشود: URL، [/login] و [http.formLogin().loginPage("/login").permitAll()]. خروج نیز برای همگان قابل دسترسی است؛
- خطوط ۱۹–۲۱: روش [configure(AuthenticationManagerBuilder auth)] را که مدیریت کاربران را بر عهده دارد، مجدداً تعریف کنید؛
- خط ۲۰: احراز هویت با استفاده از کاربران پیشتعریفشده [auth.inMemoryAuthentication()] انجام میشود. یک کاربر در اینجا با نام کاربری [user]، رمز عبور [password] و نقش [USER] تعریف میشود. میتوان همان حقوق را به کاربرانی که نقش یکسانی دارند اعطا کرد؛
8.4.12.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);
}
}
- خط ۸: آناوتیشن [@EnableAutoConfiguration] به Spring Boot (خط ۳) دستور میدهد تا پیکربندیای را که توسعهدهنده به صراحت انجام نداده است، انجام دهد؛
- خط ۹: کلاس [Application] را به یک کلاس پیکربندی Spring تبدیل میکند؛
- خط ۱۰: به سیستم دستور میدهد تا دایرکتوری حاوی کلاس [Application] را برای جستجوی کامپوننتهای Spring اسکن کند. دو کلاس [MvcConfig] و [WebSecurityConfig] به این ترتیب کشف خواهند شد زیرا دارای انوتیشن [@Configuration] هستند؛
- خط ۱۳: متد [main] از کلاس اجرایی؛
- خط ۱۴: متد استاتیک [SpringApplication.run] با کلاس پیکربندی [Application] بهعنوان پارامتر اجرا میشود. ما قبلاً با این فرایند مواجه شدهایم و میدانیم که سرور Tomcat که در وابستگیهای Maven پروژه گنجانده شده است، راهاندازی خواهد شد و پروژه روی آن مستقر خواهد شد. ما دیدهایم که چهار نمونه از URL توسط [/, /home, /login, /hello] مدیریت میشدند و برخی از آنها توسط حقوق دسترسی محافظت میشدند.
8.4.12.6. آزمون برنامه
بیایید با درخواست URL [/] شروع کنیم که یکی از چهار نمونه پذیرفتهشده URL است. این با نمای [/templates/home.html] مرتبط است:
![]() |
URL و [/] درخواستی برای همه قابل دسترسی است. به همین دلیل ما توانستیم آن را به دست آوریم. لینک [here] به شرح زیر است:
URL و [/hello] هنگام کلیک بر روی لینک درخواست خواهند شد. این مورد محافظت شده است:
URL | قانون | کد |
دسترسی بدون احراز هویت | | |
دسترسی فقط با احراز هویت |
برای دسترسی به آن باید احراز هویت شوید. سپس Spring Security مرورگر مشتری را به صفحهٔ احراز هویت هدایت میکند. بر اساس پیکربندی نمایشدادهشده، این صفحه در URL [/login] قرار دارد. این صفحه برای همه قابل دسترسی است:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
بنابراین ما به [1] میرسیم:
![]() |
کد منبع صفحه حاصل به شرح زیر است:
- در خط ۷، یک فیلد مخفی ظاهر میشود که در صفحهٔ اصلی [login.html] وجود ندارد. Thymeleaf آن را اضافه کرده است. این کد، که با نام CSRF (فریب درخواست بینسایتی) شناخته میشود، برای از بین بردن یک آسیبپذیری امنیتی طراحی شده است. این توکن باید به همراه اعتبارنامههای احراز هویت به Spring Security بازگردانده شود تا احراز هویت پذیرفته شود؛
به یاد داریم که Spring Security تنها ترکیب «کاربر/رمز عبور» را تشخیص میدهد. اگر چیزی دیگر را در [2] وارد کنیم، همان صفحه را با یک پیام خطا در [3] دریافت میکنیم. Spring Security مرورگر را به URL [http://localhost:8080/login?error] هدایت کرده است. وجود پارامتر [error] نمایش تگ را فعال کرد:
<div th:if="${param.error}">Invalid username and password.</div>
اکنون، بیایید مقادیر مورد انتظار نام کاربری/رمز عبور [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 / views ما، برای 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>
8.4.12.7. Conclusion
در مثال قبلی، میتوانستیم ابتدا وباپلیکیشن را بنویسیم و سپس آن را ایمن کنیم. Spring Security غیرمداخله گر است. امکان پیادهسازی امنیت برای یک وباپلیکیشن که قبلاً نوشته شده است وجود دارد. علاوه بر این، به نکات زیر پی بردیم:
- امکان تعریف یک صفحه احراز هویت وجود دارد؛
- احراز هویت باید با توکن CSRF صادر شده توسط Spring Security همراه باشد؛
- اگر احراز هویت ناموفق باشد، کاربر به صفحه احراز هویت هدایت میشود، به همراه یک پارامتر اضافی «error» در توکن URL؛
- اگر احراز هویت موفقیتآمیز باشد، شما به صفحهای که در زمان احراز هویت درخواست شده بود، هدایت میشوید. اگر صفحه احراز هویت مستقیماً بدون عبور از یک صفحه واسطه درخواست شود، Spring Security شما را به URL [/] هدایت میکند (این سناریو پوشش داده نشده است)؛
- شما با درخواست URL [/logout] با یک POST از سیستم خارج میشوید. سپس Spring Security ما را با پارامتر «logout» در URL به صفحه احراز هویت هدایت میکند؛
تمام این نتیجهگیریها بر اساس رفتار پیشفرض Spring Security است. این رفتار را میتوان از طریق پیکربندی با بازتعریف برخی متدهای کلاس [WebSecurityConfigurerAdapter] تغییر داد.
آموزش قبلی در ادامه کمک چندانی به ما نخواهد کرد. در واقع ما از موارد زیر استفاده خواهیم کرد:
- یک پایگاه داده برای ذخیره کاربران، رمزهای عبور و نقشهای آنها؛
- احراز هویت مبتنی بر هدر (HTTP);
آموزشهای نسبتاً کمی برای کاری که ما میخواهیم انجام دهیم در دسترس است. راهحلی که ما ارائه خواهیم داد، گردآوری کدهایی است که از اینجا و آنجا یافت شدهاند.
8.4.13. راهاندازی امنیت برای سرویس وب قرار ملاقات
8.4.13.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] باید اصلاح شوند:
![]() |
8.4.13.2. پروژه جدید STS از [métier, DAO, JPA]
پروژه [rdvmedecins-metier-dao] به شرح زیر تغییر میکند:
![]() |
- به [1]: پروژه جدید؛
- به [2]: تغییرات اعمالشده برای رفع مسائل امنیتی در یک بسته واحد، [rdvmedecins.security]، تجمیع شدهاند. این عناصر جدید به لایههای [JPA] و [DAO] تعلق دارند، اما به منظور سادگی در یک بسته واحد ترکیب شدهاند.
8.4.13.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);
}
// گیرندهها و تنظیمکنندهها
....
}
- خط ۹: کلاس، کلاس [AbstractEntity] را که پیش از این برای سایر اشیاء استفاده شده بود، گسترش میدهد؛
- خطوط ۱۳–۱۵: هیچ نام ستونی مشخص نشده است زیرا نام آنها با نام فیلدهای مرتبطشان یکسان است؛
کلاس [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 به یک User ارجاع میدهد
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
// یک UserRole به یک Role ارجاع میدهد
@ManyToOne
@JoinColumn(name = "ROLE_ID")
private Role role;
// گیرندهها و تنظیمکنندهها
...
}
- خطوط ۱۵–۱۷: تعریف کلید خارجی از جدول [USERS_ROLES] به جدول [USERS];
- خطوط ۱۹–۲۱: تعریف کلید خارجی از جدول [USERS_ROLES] به جدول [ROLES];
8.4.13.4. تغییرات در لایه [DAO]
![]() |
لایه [DAO] گسترش یافته و شامل سه ورودی جدید [Repository] میشود:
![]() |
رابط [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);
}
- خط ۹: رابط [UserRepository] رابط Spring Data [CrudRepository] (خط ۴) را گسترش میدهد؛
- خطوط ۱۲–۱۳: متد [getRoles(User user)] تمام نقشهای یک کاربر را که با [id] شناسایی شده است، بازیابی میکند
- خطوط ۱۶–۱۷: همین، اما برای کاربری که با نام کاربری و رمز عبور شناسایی میشود؛
- خط ۲۰: برای یافتن کاربر با نام کاربری او؛
رابط [RoleRepository] دسترسی به اشیاء [Role] را مدیریت میکند:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface RoleRepository extends CrudRepository<Role, Long> {
//جستجو برای یک نقش بر اساس نام آن
Role findRoleByName(String name);
}
- خط ۵: رابط [RoleRepository] رابط [CrudRepository] را گسترش میدهد؛
- خط ۸: میتوان یک نقش را با نام آن جستجو کرد؛
رابط [userRoleRepository] دسترسی به موجودیتهای [UserRole] را مدیریت میکند:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
}
- خط ۵: رابط [UserRoleRepository] صرفاً رابط [CrudRepository] را بدون افزودن هیچ متد جدیدی گسترش میدهد؛
8.4.13.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;
}
// گیرنده و تنظیمکننده
...
}
- خط ۱۰: کلاس [AppUserDetails] رابط [UserDetails] را پیادهسازی میکند؛
- خطوط ۱۵–۱۶: این کلاس یک کاربر (خط ۱۵) و مخزنی را که جزئیات آن کاربر را فراهم میکند (خط ۱۶) در بر میگیرد؛
- خطوط 22–25: سازندهای که کلاس را با یک کاربر و مخزن او نمونه سازی میکند؛
- خطوط ۲۸–۳۵: پیادهسازی متد [getAuthorities] از رابط [UserDetails]. این متد باید یک مجموعه از عناصر از نوع [GrantedAuthority] یا یک نوع مشتق را ایجاد کند. در اینجا، ما از نوع مشتق [SimpleGrantedAuthority] (خط ۳۲) استفاده میکنیم که نام یکی از نقشهای کاربر را از خط ۱۵ در خود جای داده است؛
- خطوط 31–33: ما بر روی لیست نقشهای کاربر از خط 15 تکرار میکنیم تا یک لیست از عناصر از نوع [SimpleGrantedAuthority] بسازیم؛
- خطوط ۳۸–۴۰: متد [getPassword] از رابط [UserDetails] پیادهسازی میشود. رمز عبور کاربر در خط ۱۵ بازگردانده میشود؛
- خطوط ۳۸–۴۰: متد [getUserName] از رابط [UserDetails] پیادهسازی میشود. ورود کاربر از خط ۱۵ بازگردانده میشود؛
- خطوط ۴۷–۵۰: حساب کاربری کاربر هرگز منقضی نمیشود؛
- خطوط ۵۲–۵۵: حساب کاربری کاربر هرگز مسدود نمیشود؛
- خطوط ۵۷–۶۰: اعتبارنامههای کاربر هرگز منقضی نمیشوند؛
- خطوط ۶۲–۶۵: حساب کاربری کاربر همیشه فعال است؛
Spring Security همچنین نیازمند وجود کلاسی است که رابط [AppUserDetailsService] را پیادهسازی کند:
![]() |
این رابط توسط کلاس زیر پیادهسازی شده است، [AppUserDetailsService]:
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);
}
}
- خط ۹: کلاس یک کامپوننت Spring خواهد بود و بنابراین در زمینهٔ آن در دسترس است؛
- خطوط ۱۲–۱۳: کامپوننت [UserRepository] در اینجا تزریق خواهد شد؛
- خطوط 16–25: پیادهسازی متد [loadUserByUsername] از رابط [UserDetailsService] (خط 10). پارامتر، نام کاربری کاربر است؛
- خط ۱۸: کاربر بر اساس نام کاربریاش جستجو میشود؛
- خطوط ۲۰–۲۲: اگر کاربر پیدا نشود، یک استثنا پرتاب میشود؛
- خط 24: یک شیء [AppUserDetails] ایجاد و رندر میشود. این در واقع از نوع [UserDetails] (خط 16) است؛
8.4.13.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());
// زمینهٔ اسپرینگ
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();
}
}
- خط ۱۷: کلاس منتظر سه آرگومان است که یک کاربر را تعریف میکنند: نام کاربری، رمز عبور و نقش؛
- خطوط ۲۵–۲۷: سه پارامتر بازیابی میشوند؛
- خط ۲۹: کانکست Spring از کلاس پیکربندی [DomainAndPersistenceConfig] ساخته میشود. این کلاس از قبل در پروژه اولیه وجود داشت. باید به شرح زیر بهروزرسانی شود:
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities", "rdvmedecins.security" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
....
}
- خط ۱: باید مشخص شود که اکنون کامپوننتهای [Repository] در پکیج [rdvmedecins.security] وجود دارند؛
- خط ۴: باید اشاره شود که اکنون اجزای JPA در بسته [rdvmedecins.security] وجود دارند؛
بیایید به کد ایجاد کاربر بازگردیم:
- خطوط ۳۰–۳۲: ما ارجاعات سه موجودیت [Repository] را که ممکن است برای ایجاد کاربر مفید باشند، بازیابی میکنیم؛
- خط ۳۴: بررسی میکنیم که آیا نقش از قبل وجود دارد یا خیر؛
- خطوط ۳۶–۳۸: اگر چنین نباشد، آن را در پایگاه داده ایجاد میکنیم. نام آن به شکل [ROLE_XX] خواهد بود؛
- خط ۴۰: بررسی میکنیم که آیا نام کاربری از قبل وجود دارد یا خیر؛
- خطوط ۴۲–۴۹: اگر نام کاربری وجود نداشته باشد، آن را در پایگاه داده ایجاد میکنیم؛
- خط ۴۴: رمز عبور رمزگذاری میشود. در اینجا از کلاس Spring Security به نام [BCrypt] (خط ۴) استفاده میکنیم. بنابراین به آرشیوهای این فریمورک نیاز داریم. فایل [pom.xml] شامل وابستگی جدید است:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- خط ۴۶: کاربر در پایگاه داده ذخیره میشود؛
- خط ۴۸: همانطور که رابطه آنها را به نقششان پیوند میدهد؛
- خطوط ۵۱–۵۷: اگر ورود (login) از قبل وجود داشته باشد – در این صورت بررسی میکنیم که آیا نقشی که میخواهیم به آنها اختصاص دهیم، از قبل در میان نقشهایشان وجود دارد یا خیر؛
- خطوط ۵۹–۶۱: اگر نقش مورد جستجو یافت نشود، یک سطر در جدول [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()));
//بررسی نقش مدیر / مدیر
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()));
//بررسی نقش مدیر / مدیر
@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);
}
}
}
- خطوط ۲۷–۳۴: آزمون بصری. تمام کاربران به همراه نقشهایشان نمایش داده میشوند؛
- خطوط ۳۶–۴۶: ما بررسی میکنیم که کاربر [admin] رمز عبور [admin] و نقش [ROLE_ADMIN] را با استفاده از مخزن [UserRepository] دارد؛
- خط ۴۱: [admin] رمز عبور متن ساده است. در پایگاه داده، با استفاده از الگوریتم BCrypt رمزگذاری شده است. روش [BCrypt.checkpw] برای تأیید اینکه گذرواژهٔ متن ساده، پس از رمزگذاری، واقعاً با گذرواژهٔ موجود در پایگاه داده یکسان است، استفاده میشود؛
- خطوط ۴۸–۵۹: ما تأیید میکنیم که کاربر [admin] رمز عبور [admin] و نقش [ROLE_ADMIN] را با استفاده از سرویس [appUserDetailsService] دارد؛
تستها با لاگهای زیر با موفقیت اجرا شدند:
8.4.13.7. نتیجهگیری موقت
کلاسهای لازم برای Spring Security با کمترین تغییرات در پروژه اصلی اضافه شدند. برای خلاصه:
- افزودن وابستگی به Spring Security در فایل [pom.xml];
- ایجاد سه جدول اضافی در پایگاه داده؛
- ایجاد اشیاء JPA و کامپوننتهای Spring در پکیج [rdvmedecins.security]؛
این سناریوی بسیار مطلوب ناشی از این واقعیت است که سه جدول افزودهشده به پایگاه داده، مستقل از جدولهای موجود هستند. حتی میتوانستند در یک پایگاه داده جداگانه قرار گیرند. این امر ممکن بود زیرا تصمیم گرفته شد که یک کاربر به طور مستقل از پزشکان و مشتریان وجود دارد. اگر مورد دوم (پزشکان) کاربران بالقوه بودند، لازم بود که لینکهایی بین جدول [USERS] و جداول [MEDECINS] و [CLIENTS] ایجاد شود. این امر سپس تأثیر قابل توجهی بر پروژه موجود میگذاشت.
8.4.13.8. پروژه STS در لایه [web]
![]() |
پروژه [rdvmedecins-webjson] از [1] به شرح زیر تکامل مییابد:
![]() |
تغییرات اصلی باید در بسته [rdvmedecins.web.config] انجام شود، جایی که Spring Security نیاز به پیکربندی دارد. تغییرات جزئی دیگری در کلاسهای [AppConfig] و [ApplicationModel] وجود دارد. ما قبلاً با یک کلاس پیکربندی 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");
}
}
ما همان رویه را دنبال خواهیم کرد:
- خط ۱۱: تعریف یک کلاس که از کلاس [WebSecurityConfigurerAdapter] ارث میبرد؛
- خط ۱۳: تعریف یک متد [configure(HttpSecurity http)] که حقوق دسترسی به متدهای مختلف URL سرویس وب را مشخص میکند؛
- خط ۱۹: متد [configure(AuthenticationManagerBuilder auth)] را تعریف کنید که کاربران و نقشهای آنها را مشخص میکند؛
امنیت اسپرینگ از طریق کلاس [SecurityConfig] پیکربندی میشود:
package rdvmedecins.web.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import rdvmedecins.security.AppUserDetailsService;
import rdvmedecins.web.models.ApplicationModel;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AppUserDetailsService appUserDetailsService;
@Autowired
private ApplicationModel application;
@Override
protected void configure(AuthenticationManagerBuilder registry) throws Exception {
//احراز هویت توسط bean [appUserDetailsService] انجام میشود
// رمز عبور با استفاده از الگوریتم هش BCrypt رمزگذاری میشود
registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//CSRF
http.csrf().disable();
// اپلیکیشن امن؟
if (application.isSecured()) {
// رمز عبور از طریق هدر Authorisation: Basic xxxx ارسال میشود
http.httpBasic();
//روش HTTP OPTIONS باید برای همه مجاز باشد
http.authorizeRequests() //
.antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
//تنها نقش ADMIN میتواند از برنامه استفاده کند
http.authorizeRequests() //
.antMatchers("/", "/**") // تمام URL
.hasRole("ADMIN");
// هیچ جلسهای وجود ندارد
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
}
- خط ۱۵: کلاس [SecurityConfig] یک کلاس پیکربندی Spring است؛
- خط ۱۶: برای راهاندازی امنیت پروژه؛
- خطوط ۱۹–۲۰: کلاس [AppUserDetails] که دسترسی کاربران به برنامه را فراهم میکند، تزریق میشود؛
- خطوط ۲۱–۲۲: کلاس [ApplicationModel] که بهعنوان حافظه پنهان برای برنامه وب عمل میکند، تزریق میشود. ما تصمیم گرفتهایم از این کلاس در اینجا نیز استفاده کنیم تا وباپلیکیشن را در یک مکان واحد پیکربندی کنیم. این کلاس است که متغیر بولی [isSecured] را در خط ۳۶ تعریف میکند. این متغیر بولی وباپلیکیشن را یا ایمن میکند (true) یا ایمن نمیکند (false)؛
- خطوط ۲۵–۲۹: متد [configure(HttpSecurity http)] کاربران و نقشهای آنها را تعریف میکند. این متد یک پارامتر از نوع [AuthenticationManagerBuilder] را میپذیرد. این پارامتر با دو مورد اطلاعات (خط ۲۸) غنیسازی میشود:
- یک مرجع به سرویس [appUserDetailsService] در خط ۲۰، که دسترسی به کاربران ثبتنامشده را فراهم میکند. باید توجه داشت که این موضوع، یعنی ذخیرهسازی آنها در یک پایگاه داده، به صراحت بیان نشده است. بنابراین، آنها میتوانند در یک حافظه پنهان (cache)، توسط یک سرویس وب بازگردانده شوند و غیره، ذخیره شوند.
- نوع رمزنگاری است که برای رمز عبور استفاده میشود. باید توجه داشت که در اینجا از الگوریتم BCrypt استفاده شده است؛
- خطوط ۳۸–۴۷: متد [configure(HttpSecurity http)] حقوق دسترسی به توکنهای URL سرویس وب را تعریف میکند؛
- خط ۳۴: همانطور که در پروژه مقدماتی دیدیم، Spring Security به طور پیشفرض یک توکن CSRF (فریب درخواست بینسایتی) را مدیریت میکند که کاربر برای احراز هویت باید آن را به سرور بازگرداند. در اینجا، این مکانیزم غیرفعال شده است. این موضوع، در ترکیب با تنظیم بولی (isSecured=false)، به وباپلیکیشن اجازه میدهد تا بدون امنیت استفاده شود؛
- خط ۳۸: ما احراز هویت را از طریق هدر HTTP فعال میکنیم. کلاینت باید هدر زیر را ارسال کند:
که در آن «code» رمزگذاری Base64 رشته «login:password» است. به عنوان مثال، رمزگذاری Base64 رشته «admin:admin» برابر است با «YWRtaW46YWRtaW4=». بنابراین، کاربری با نام کاربری [admin] و رمز عبور [admin]، برای احراز هویت، سربرگ زیر را ارسال خواهد کرد: HTTP
- خطوط ۴۰–۴۲: نشان میدهند که تمام منابع URL سرویس وب برای کاربرانی با نقش [ROLE_ADMIN] قابل دسترسی هستند. این بدان معناست که کاربری که این نقش را ندارد، نمیتواند به سرویس وب دسترسی پیدا کند؛
- خط ۴۷: رمز عبور کاربر ممکن است در یک جلسه (session) ذخیره شود یا نشود. اگر ذخیره شود، کاربر فقط در اولین بار نیاز به احراز هویت دارد. در دفعات بعدی، از او برای ارائه اعتبارنامههایش سؤال نخواهد شد. در اینجا، حالت بدون جلسه (sessionless) انتخاب شده است. هر درخواست باید با اعتبارنامههای امنیتی همراه باشد؛
کلاس [AppConfig] که کل برنامه را پیکربندی میکند، به شرح زیر تغییر یافته است:
![]() |
package rdvmedecins.web.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
}
- این تغییر در خط ۱۱ انجام شده است: کلاس پیکربندی [SecurityConfig] اضافه شده است؛
در نهایت، کلاس [ApplicationModel] با یک متغیر بولی گسترش مییابد:
@Component
public class ApplicationModel implements IMetier {
...
// دادههای پیکربندی
private boolean secured = false;
public boolean isSecured() {
return secured;
}
- خط ۶: متغیر بولی [secured] بسته به اینکه آیا میخواهید امنیت را فعال کنید یا خیر، روی [true / false] تنظیم میشود.
8.4.13.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 را تولید کنیم، میتوانیم سرویس وب اکنون امن را راهاندازی کنیم:
@Component
public class ApplicationModel implements IMetier {
...
private boolean secured = true;
سپس، با استفاده از کلاینت کروم [Advanced Rest Client]، فهرست تمام پزشکان را درخواست میکنیم:
![]() |
- در [1]، ما URL را برای پزشکان درخواست میکنیم؛
- در [2]، با استفاده از روش GET؛
- در [3]، هدر احراز هویت HTTP را ارائه میدهیم. کد [YWRtaW46YWRtaW4=] رمزگذاری Base64 رشته [admin:admin] است؛
- در [4]، ما فرمان HTTP را ارسال میکنیم؛
پاسخ سرور به شرح زیر است:
![]() |
- در [1]، هدر احراز هویت HTTP;
- در [2]، سرور پاسخ jSON را بازمیگرداند؛
- در [3]، فهرستی از سربرگهای HTTP مربوط به امنیت برنامه وب؛
ما در واقع فهرست پزشکان را دریافت میکنیم:
![]() |
حال بیایید یک درخواست HTTP با یک هدر احراز هویت نادرست ارسال کنیم. پاسخ به شرح زیر است:
![]() |
- در [1] و [3]: هدر احراز هویت HTTP؛
- به [2]: پاسخ سرویس وب؛
اکنون، بیایید کاربر 'user' / 'user' را امتحان کنیم. این کاربر وجود دارد اما به سرویس وب دسترسی ندارد. اگر برنامه رمزگذاری Base64 را با دو آرگومان [user user] اجرا کنیم:
![]() |
نتیجه زیر را دریافت میکنیم:
![]() |
- در [1] و [3]: هدر احراز هویت HTTP;
- به [2]: پاسخ سرویس وب. این با مورد قبلی که [401 Unauthorized] بود، متفاوت است. در این مورد، کاربر با موفقیت احراز هویت شد اما مجوزهای کافی برای دسترسی به URL را ندارد؛
یک سرویس وب امن اکنون عملیاتی است. ما آن را گسترش خواهیم داد تا درخواستهای بیندامنهای را مجاز کند. این نیازمندی در سند [Tutoriel AngularJS / Spring 4] شناسایی شده است و اگرچه در اینجا کاربرد ندارد، با این حال به آن خواهیم پرداخت.
8.4.14. پیادهسازی درخواستهای بیندامنهای
بیایید مسئله درخواستهای بیندامنهای را بررسی کنیم. در سند [Tutoriel AngularJS / Spring 4]، یک برنامهٔ کلاینت/سرور توسعه یافته است که در آن کلاینت برنامهای به نام AngularJS است:
![]() |
- صفحات HTML / CSS / JS از برنامه Angular توسط سرور [1] ارائه میشوند؛
- در [2]، سرویس [dao] درخواستی به سرور دیگر، یعنی سرور [2]، ارسال میکند. با این حال، این کار توسط مرورگری که برنامه Angular را اجرا میکند، ممنوع است زیرا یک آسیبپذیری امنیتی محسوب میشود. این برنامه فقط میتواند از سروری که از آن آمده است، درخواست کند، یعنی سرور [1];
در واقع، این گفته که مرورگر مانع از ارسال درخواست توسط اپلیکیشن Angular به سرور [2] میشود، نادرست است. در عمل، این مرورگر به آن سرور درخواست میدهد تا بپرسد آیا به کلاینتی که از دامنهٔ خود آن سرور نیست اجازه میدهد تا به آن درخواست ارسال کند یا خیر. این تکنیک اشتراکگذاری با نام اشتراکگذاری منابع فرامنشأ (CORS) شناخته میشود. سرور [2] با ارسال هدرهای خاص، اجازه را صادر میکند.
برای نشان دادن مشکلاتی که ممکن است پیش بیاید، قصد داریم یک برنامهٔ کلاینت/سرور بسازیم که در آن:
- سرور، وبسرور ما / jSON خواهد بود؛
- کلاینت یک صفحه ساده HTML خواهد بود که حاوی کد جاوااسکریپت است و درخواستهایی را به وبسرور / jSON ارسال میکند؛
8.4.14.1. پروژه کلاینت
![]() |
این پروژه یک پروژه 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>istia.st</groupId>
<artifactId>rdvmedecins-webjson-client-cors</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-client-cors</name>
<description>Client for webjson server</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
<relativePath /> <!-- جستجوی والد در مخزن -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.rdvmedecins.Client</start-class>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- spring MVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
- خطوط ۱۴–۱۹: این یک پروژه Spring Boot است؛
- خطوط ۲۹–۳۲: وابستگی [spring-boot-starter-web] استفاده میشود که شامل سرور Tomcat و Spring MVC است؛
صفحه HTML به شرح زیر است:
![]() |
این توسط کد زیر تولید میشود:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Spring MVC</title>
<script type="text/javascript" src="/js/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/client.js"></script>
</head>
<body>
<h2>Client du service web / jSON</h2>
<form id="formulaire">
<!-- متد HTTP -->
Méthode HTTP :
<!-- -->
<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<!-- -->
<input type="radio" id="post" name="method" value="post" />POST
<!-- URL -->
<br /> <br />URL cible : <input type="text" id="url" size="30"><br />
<!-- ارزش ارسالشده -->
<br /> Chaîne jSON à poster : <input type="text" id="posted" size="50" />
<!-- دکمه ارسال -->
<br /> <br /> <input type="submit" value="Valider" onclick="javascript:requestServer(); return false;"></input>
</form>
<hr />
<h2>Réponse du serveur</h2>
<div id="response"></div>
</body>
</html>
- خط ۶: کتابخانه jQuery وارد میشود؛
- خط ۷: ما بخشی از کدی را که قرار است بنویسیم وارد میکنیم؛
کد [client.js] به شرح زیر است:
// دادههای جهانی
var url;
var posted;
var response;
var method;
function requestServer() {
// ما دادههای فرم را بازیابی میکنیم
var urlValue = url.val();
var postedValue = posted.val();
method = document.forms[0].elements['method'].value;
// به صورت دستی فراخوانی AJAX انجام دهید
if (method === "get") {
doGet(urlValue);
} else {
doPost(urlValue, postedValue);
}
}
function doGet(url) {
// انجام فراخوانی Ajax بهصورت دستی
$.ajax({
headers : {
'احراز هویت: 'Basic YWRtaW46YWRtaW4='
},
url : 'http://'localhost:8080' + url,
type : 'GET',
dataType : 'tex/plain',
beforeSend : function() {
},
success : function(data) {
// متن نتیجه
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// خطای سیستم
response.text(jqXHR.responseText);
}
})
}
function doPost(url, posted) {
//یک فراخوانی Ajax بهصورت دستی انجام میشود
$.ajax({
headers : {
'احراز هویت: 'Basic YWRtaW46YWRtaW4='
},
url : 'http://'localhost:8080' + url,
type : 'POST',
contentType : 'application/json',
data : posted,
dataType : 'tex/plain',
beforeSend : function() {
},
success : function(data) {
// متن نتیجه
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// خطای سیستم
response.text(jqXHR.responseText);
}
})
}
// هنگام بارگذاری سند
$(document).ready(function() {
//بازیابی ارجاعات به اجزای صفحه
url = $("#url");
posted = $("#posted");
response = $("#response");
});
درک این کد را به خواننده واگذار میکنیم. همهچیز قبلاً یکبار یا چند بار دیده شده است. با این حال، برخی خطوط نیاز به توضیح دارند:
- خط ۱۱:
- [document] به سندی اشاره دارد که توسط مرورگر بارگذاری شده و با نام DOM (مدل شیء سند) شناخته میشود،
- [document.forms[0]] به اولین فرم در سند اشاره دارد؛ یک سند ممکن است حاوی چندین فرم باشد. در این مورد، تنها یک فرم وجود دارد،
- [document.forms[0].elements['method']] به عنصر فرم اشاره دارد که دارای صفت [name='method'] است. دو مورد از آنها وجود دارد:
<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<input type="radio" id="post" name="method" value="post" />POST
- خط ۱۱:
- [document.forms[0].elements['method'].value] مقداری است که برای کامپوننت با ویژگی [name='method'] ارسال خواهد شد. ما میدانیم که مقدار ارسالشده، مقدار ویژگی [value] دکمه رادیویی انتخابشده است. بنابراین، در اینجا این مقدار یکی از رشتههای ['get', 'post'] خواهد بود؛
- خطوط ۲۳–۲۵: ما در حال ارتباط با سرور هستیم که به هدر HTTP [Authorization: Basic code] نیاز دارد. ما این هدر را برای کاربر [admin / admin] ایجاد میکنیم، که تنها فرد مجاز برای استعلام از سرور است؛
- خط ۲۶: کاربر URL از نوع [/getAllMedecins, /supprimerRv, ...] را وارد خواهد کرد. بنابراین این URL باید تکمیل شوند؛
- خط ۲۸: سرور مقدار jSON را که یک رشته متنی است، برمیگرداند. نوع [text/plain] به عنوان نوع نتیجه مشخص شده است تا دقیقاً همانطور که دریافت شده نمایش داده شود؛
- خط ۳۳: نمایش پاسخ متنی سرور؛
- خط ۳۹: نمایش هرگونه پیام خطا در قالب متن؛
- خط ۵۲: برای نشان دادن اینکه کلاینت در حال ارسال jSON است؛
در برنامهٔ کلاینت/سرور ساختهشده:
- کلاینت یک برنامه وب است که در URL [http://localhost:8081] در دسترس است. این برنامهای است که ما در حال حاضر در حال ساخت آن هستیم؛
- سرور یک برنامه وب است که در URL [http://localhost:8080] در دسترس است. این وبسرور ما / jSON است؛
از آنجا که کلاینت از طریق پورت یکسان با سرور دسترسی پیدا نمیکند، مسئله درخواستهای بیندامنهای پیش میآید. [http://localhost:8080] و [http://localhost:8081] دو دامنهٔ متفاوت هستند.
اپلیکیشن Spring Boot یک برنامه کنسول است که توسط کلاس اجرایی زیر، [Client]، راهاندازی میشود:
package istia.st.rdvmedecins;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
@EnableWebMvc
public class Client extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(Client.class, args);
}
//صفحات ایستا
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations(new String[] { "classpath:/static/" });
}
// QZXW2HTML پیکربندی CZGlzcGF0Y2hlclNlcnZsZXQZQX
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
// سرور Tomcat تعبیهشده
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8081);
}
}
- خط 14: کلاس [Client] یک کلاس پیکربندی Spring است؛
- خط ۱۵: یک برنامه Spring MVC پیکربندی شده است. این anotation باعث اجرای تعدادی پیکربندی خودکار میشود؛
- خط 16: برای بازنویسی برخی مقادیر پیشفرض فریمورک Spring MVC، باید کلاس [WebMvcConfigurerAdapter] را ارثبری کنید؛
- خطوط ۲۳–۲۶: متد [addResourceHandlers] به شما امکان میدهد دایرکتوریهایی را که منابع استاتیک برنامه (HTML، CSS، JS و غیره) در آنها قرار دارند، مشخص کنید. در اینجا، ما دایرکتوری [static] را که در کلاسپث پروژه قرار دارد، مشخص میکنیم:
![]() |
- خطوط ۲۹–۳۷: پیکربندی bean [dispatcherServlet]، که به servlet بهاری MVC اشاره دارد؛
- خطوط ۴۰–۴۳: سرور Tomcat تعبیهشده روی پورت ۸۰۸۱ اجرا خواهد شد؛
8.4.14.2. URL [/getAllMedecins]
ما در حال راهاندازی هستیم:
- سرور وب/JSON روی پورت 8080؛
- کلاینت این سرور روی پورت 8081؛
سپس ما URL [http://localhost:8081/client.html] [1] را درخواست میکنیم:
![]() |
- به [2]، ما یک GET را روی URL [http://localhost:8080/getAllMedecins] انجام میدهیم؛
ما پاسخی از سرور دریافت نمیکنیم. وقتی به کنسول توسعهدهنده (Ctrl-Shift-I) نگاه میکنیم، با خطایی مواجه میشویم:
![]() |
- در [1]، ما روی زبانه [Network] هستیم؛
- در [2]، میتوانیم ببینیم که درخواست HTTP که انجام شده، [GET] نیست بلکه [OPTIONS] است. در مورد درخواست بیندامنهای، مرورگر با ارسال درخواستی برای HTTP [OPTIONS] به سرور بررسی میکند که تعدادی شرط برآورده شده است. در این مورد، درخواستها همانهایی هستند که با نقاط [5-6] نشان داده شدهاند؛
- در [5]، مرورگر میپرسد که آیا هدف URL از طریق یک GET قابل دسترسی است یا خیر. سرآیند درخواست [Access-Control-Request-Method]، پاسخی را با سرآیند HTTP [Access-Control-Allow-Methods] درخواست میکند که نشان دهد روش درخواستی پذیرفته شده است؛
- در [5]، مرورگر هدر HTTP [Origin: http://localhost:8081] را ارسال میکند. این هدر درخواست پاسخی را در یک هدر HTTP [Access-Control-Allow-Origin] ارسال میکند که نشان میدهد منشأ مشخصشده پذیرفته شده است؛
- در [6]، مرورگر میپرسد که آیا سربرگهای HTTP، [accept] و [authorization] پذیرفته میشوند یا خیر. سربرگ درخواست [Access-Control-Request-Headers] منتظر پاسخی با سربرگ HTTP [Access-Control-Allow-Headers] است که نشان دهد سربرگهای درخواستی پذیرفته شدهاند؛
- در [3] خطایی رخ میدهد. کلیک بر روی آیکون منجر به خطای [4] میشود؛
- در [4]، پیام نشان میدهد که سرور هدر HTTP [Access-Control-Allow-Origin] را که مشخص میکند آیا منبع درخواست پذیرفته شده است یا خیر، ارسال نکرده است؛
- در [7]، میتوانیم ببینیم که سرور واقعاً این هدر را ارسال نکرده است. در نتیجه، مرورگر از انجام درخواست HTTP GET که در ابتدا درخواست شده بود، خودداری کرد؛
ما باید وبسرور / jSON را اصلاح کنیم. ما یک تغییر اولیه در [ApplicationModel] انجام میدهیم که یکی از عناصر پیکربندی سرویس وب است:
![]() |
@Component
public class ApplicationModel implements IMetier {
...
// دادههای پیکربندی
private boolean corsAllowed = true;
private boolean secured = true;
...
public boolean isCorsAllowed() {
return corsAllowed;
}
- خط ۶: ما یک متغیر بولی ایجاد میکنیم تا نشان دهد که آیا کلاینتهای خارج از دامنه سرور پذیرفته میشوند یا خیر؛
- خطوط ۱۰–۱۲: متد دسترسی به این اطلاعات؛
سپس یک کنترلر جدید Spring به نام MVC ایجاد میکنیم:
![]() |
کلاس [RdvMedecinsCorsController] به شرح زیر است:
package rdvmedecins.web.controllers;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import rdvmedecins.web.models.ApplicationModel;
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// گزینههای ارسال به کلاینت
public void sendOptions(String origin, HttpServletResponse response) {
// آیا CORS مجاز است؟
if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
return;
}
// تنظیم هدر CORS
response.addHeader("Access-Control-Allow-Origin", origin);
// سرورهای خاصی مجاز هستند
response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
// GET مجاز است
response.addHeader("Access-Control-Allow-Methods", "GET");
}
// فهرست پزشکان
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
sendOptions(origin, response);
}
}
- خطوط ۱۲–۱۳: کلاس [RdvMedecinsCorsController] یک کنترلر Spring است؛
- خطوط ۳۳–۳۶: یک اکشن تعریف کنید که هنگام درخواست با دستورات HTTP و [OPTIONS]، رسیدگی به URL و [/getAllMedecins] را بر عهده دارد؛
- خط ۳۴: متد [getAllMedecins] پارامترهای زیر را میپذیرد:
- شیء [@RequestHeader(value = "Origin", required = false)] که سربرگ درخواست HTTP [Origin] را بازیابی میکند. این سربرگ توسط مبدأ درخواست ارسال شده است:
مشخص شده است که هدر HTTP [Origin] اختیاری [required = false] است. در این حالت، اگر هدر وجود نداشته باشد، پارامتر [String origin] مقدار null را خواهد داشت. با [required = true] بهعنوان مقدار پیشفرض، در صورت عدم وجود هدر، یک استثنا پرتاب میشود. ما میخواستیم از این سناریو اجتناب کنیم؛
- خط ۳۴:
- شیء [HttpServletResponse response]، که به کلاینتی که درخواست را ارسال کرده است، ارسال خواهد شد؛
این دو پارامتر توسط Spring تزریق میشوند؛
- خط ۳۵: پردازش درخواست به متد در خطوط ۱۹–۳۰ واگذار میشود؛
- خطوط ۱۵–۱۶: شیء [ApplicationModel] تزریق میشود؛
- خطوط ۲۱–۲۳: اگر برنامه برای پذیرش درخواستهای بیندامنهای پیکربندی شده باشد، و اگر فرستنده هدر HTTP [Origin] را ارسال کرده باشد، و اگر این مبدا با [http://localhost] شروع شود، در این صورت درخواست بیندامنهای پذیرفته میشود؛ در غیر این صورت، رد میشود؛
- خط 25: اگر کلاینت در دامنه [http://localhost:port] باشد، هدر HTTP ارسال میشود:
که به این معنی است که سرور منشأ کلاینت را میپذیرد؛
- خط ۲۵: ما دو هدر خاص HTTP را در درخواست HTTP [OPTIONS] شناسایی کردهایم:
در پاسخ به هدر HTTP [Access-Control-Request-X]، سرور با هدر HTTP [Access-Control-Allow-X] پاسخ میدهد که در آن مشخص میکند چه چیزی مجاز است. خطوط ۲۳ تا ۲۶ صرفاً درخواست کلاینت را تکرار میکنند تا نشان دهند که پذیرفته شده است؛
اکنون برای آزمایشهای بیشتر آمادهایم. نسخه جدید سرویس وب را راهاندازی میکنیم و متوجه میشویم که مشکل همچنان حلنشده باقی مانده است. هیچ چیز تغییر نکرده است. اگر در خط ۳۵ بالا یک خروجی کنسول اضافه کنیم، هرگز نمایش داده نمیشود، که این نشان میدهد متد [getAllMedecins] در خط ۳۴ هرگز فراخوانی نمیشود.
پس از کمی بررسی، متوجه میشویم که Spring MVC خود، با استفاده از پردازش پیشفرض، دستورات HTTP و [OPTIONS] را مدیریت میکند. در نتیجه، همیشه Spring پاسخ میدهد و هرگز متد [getAllMedecins] در خط ۳۴ فراخوانی نمیشود. این رفتار پیشفرض Spring MVC را میتوان تغییر داد. ما کلاس موجود [WebConfig] را اصلاح میکنیم:
![]() |
package rdvmedecins.web.config;
...
import org.springframework.web.servlet.DispatcherServlet;
@Configuration
public class WebConfig {
//پیکربندی DispatcherServlet برای سربرگها CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
//نگاشت jSON
...
- خطوط ۱۰–۱۱: بیان [dispatcherServlet] برای تعریف سرولتی که درخواستهای کلاینت را مدیریت میکند، استفاده میشود. در اینجا، نوع آن [DispatcherServlet] است، سرولت MVC چارچوب اسپرینگ؛
- خط ۱۲: یک نمونه از نوع [DispatcherServlet] ایجاد میشود؛
- خط ۱۳: به سرولت دستور داده میشود که دستورات HTTP و [OPTIONS] را به برنامه فوروارد کند؛
- خط ۱۴: سرولت همانطور که توضیح داده شده پیکربندی میشود؛
ما تستها را با این پیکربندی جدید دوباره اجرا میکنیم. نتیجه زیر را به دست میآوریم:
![]() |
- در [1]، میتوانیم ببینیم که دو درخواست، HTTP و URL، به [http://localhost:8080/getAllMedecins] وجود دارد؛
- در [2]، درخواست [OPTIONS]؛
- در [3]، سه هدر HTTP که ما همیناکنون در پاسخ سرور پیکربندی کردهایم؛
اکنون درخواست دوم را بررسی کنیم:
![]() |
- به [1]، درخواستی که در حال بررسی است؛
- به [2]، که همان درخواست GET است. به لطف درخواست اول، [OPTIONS]، مرورگر اطلاعاتی را که درخواست کرده بود دریافت کرد. اکنون در حال اجرای درخواست [GET] است که در ابتدا درخواست شده بود؛
- در [3]، پاسخ سرور؛
- در [4]، سرور jSON را ارسال میکند؛
- در [5]، خطایی رخ داده است؛
- در [6]، پیام خطا؛
توضیح آنچه در اینجا رخ داده دشوارتر است. پاسخ سرور [3] طبیعی است [HTTP/1.1 200 OK]. بنابراین باید سند درخواستی را در اختیار داشته باشیم. ممکن است سرور واقعاً سند را ارسال کرده باشد، اما مرورگر از استفادهٔ آن جلوگیری میکند زیرا نیاز دارد پاسخ به درخواست GET همچنین شامل سربرگهای HTTP و [Access-Control-Allow-Origin:http://localhost:8081] باشد.
ما کنترلکننده [RdvMedecinsController] را به شرح زیر اصلاح میکنیم:
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
...
// فهرست پزشکان
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins(HttpServletResponse httpServletResponse,
@RequestHeader(value = "Origin", required = false) String origin) throws JsonProcessingException {
// پاسخ
Response<List<Medecin>> response;
// سربرگها CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
//وضعیت درخواست
...
- خطوط ۱–۲: کنترلکننده [RdvMedecinsCorsController] تزریق میشود؛
- خطوط ۷–۸: ما شیء HttpServletResponse را که پاسخ ارسالی به کلاینت را در بر میگیرد، و هدرهای HTTP و [Origin] را به پارامترهای متد [getAllMedecins] تزریق میکنیم؛
- خط ۱۲: متد [sendOptions] از کنترلر [RdvMedecinsCorsController] فراخوانی میشود؛ این همان متدی است که برای پردازش درخواست HTTP [OPTIONS] فراخوانی شده بود. بنابراین همان هدرها (HTTP) را مانند آن درخواست ارسال خواهد کرد؛
پس از این تغییر، نتایج به شرح زیر است:
![]() |
ما در واقع فهرست پزشکان را به دست آوردیم.
8.4.14.3. سایر URL و [GET]
اکنون سایر پرسوجوهای URL را که از طریق GET بازیابی شدهاند، نشان میدهیم. در کنترلرها، کد مربوط به اقداماتی که آنها را پردازش میکنند، از الگوی اقداماتی که قبلاً URL و [/getAllMedecins] را پردازش میکردند، پیروی میکند. خواننده میتواند کد را در مثالهای ارائهشده در این سند بررسی کند. در اینجا یک مثال آمده است:
در [RdvMedecinsCorsController]
// فهرست قرارهای ملاقات پزشک
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
public void getRvMedecinJour(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
sendOptions(origin, response);
}
در [RdvMedecinsController]
// فهرست قرارهای ملاقات پزشک
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour,
HttpServletResponse httpServletResponse, @RequestHeader(value = "Origin", required = false) String origin)
throws JsonProcessingException {
// پاسخ
Response<List<Rv>> response = null;
boolean erreur = false;
// سربرگها CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
//وضعیت درخواست
...
در اینجا چند تصویر از اجرای برنامه آورده شده است:
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
8.4.14.4. URL [POST]
بیایید مورد زیر را در نظر بگیریم:
![]() |
- ما یک POST [1] به URL [2] انجام میدهیم؛
- در [3]، مقدار ارسالشده. این یک رشته jSON است؛
- در مجموع، ما قصد داریم قرار ملاقات با مقدار [id] 100 را حذف کنیم؛
در این مرحله هیچ کدی را تغییر نمیدهیم. نتیجهٔ بهدستآمده به شرح زیر است:
![]() |
- در [1]، مانند درخواستهای [GET]، یک درخواست [OPTIONS] توسط مرورگر انجام میشود؛
- برای [2]، درخواست مجوز دسترسی برای درخواستی برای [POST] ارسال میشود. قبلاً این [GET] بود؛
- در [3]، درخواست مجوز برای ارسال سربرگهای HTTP و [accept, authorization, content-type] میشود. قبلاً، فقط دو سربرگ اول گنجانده شده بودند؛
ما روش [RdvMedecinsCorsController.sendOptions] را به شرح زیر تغییر میدهیم:
public void sendOptions(String origin, HttpServletResponse response) {
//آیا CORS مجاز است؟
if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
return;
}
// تنظیم هدر CORS
response.addHeader("Access-Control-Allow-Origin", origin);
// سرور برخی هدرها را مجاز میداند
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// GET مجاز است
response.addHeader("Access-Control-Allow-Methods", "GET, POST");
}
- خط ۹: سربرگهای HTTP و [Content-Type] اضافه شدهاند (حالت حروف مهم نیست)؛
- خط ۱۱: روش HTTP [POST] اضافه شده است؛
این بدان معناست که متدهای [POST] به همان شیوهای که درخواستهای [GET] پردازش میشوند، مدیریت میگردند. در اینجا مثالی از URL [/supprimerRv] آمده است:
در [RdvMedecinsController]
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse httpServletResponse,
@RequestHeader(value = "Origin", required = false) String origin) throws JsonProcessingException {
//پاسخ
Response<Void> response = null;
boolean erreur = false;
// سربرگها CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
//وضعیت برنامه
if (messages != null) {
...
در [RdvMedecinsCorsController]
@RequestMapping(value = "/supprimerRv", method = RequestMethod.OPTIONS)
public void supprimerRv(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
sendOptions(origin, response);
}
نتیجهٔ بهدستآمده به شرح زیر است:
![]() |
برای URL و [/ajouterRv]، نتیجه به شرح زیر است:
![]() |
8.4.14.5. Conclusion
برنامه ما اکنون از درخواستهای بیندامنهای پشتیبانی میکند. این درخواستها را میتوان از طریق پیکربندی در کلاس [ApplicationModel] فعال یا غیرفعال کرد:
// دادههای پیکربندی
private boolean corsAllowed = false;
8.5. برنامهٔ مشتری سرویس وب / jSON
بیایید به معماری کلی برنامهای که قصد داریم بنویسیم بازگردیم:
![]() |
بخش بالایی نمودار نوشته شده است. این وب سرور / jSON است. اکنون به سراغ بخش پایینی میرویم و از لایه آن [DAO] شروع میکنیم. این را مینویسیم و سپس با یک کلاینت کنسول آن را تست میکنیم. معماری تست به شرح زیر خواهد بود:
![]() |
8.5.1. پروژه کلاینت کنسول
پروژه کلاینت کنسول STS به شرح زیر خواهد بود:
![]() |
8.5.2. پیکربندی Maven
فایل [pom.xml] کلاینت کنسول به شرح زیر است:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.rdvmedecins</groupId>
<artifactId>rdvmedecins-webjson-client-console</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-webjson-client-console</name>
<description>Client console du serveur web / jSON</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
<relativePath /> <!-- جستجوی والد از مخزن -->
</parent>
<dependencies>
<!-- اسپرینگ -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- jSON کتابخانهای که توسط Spring استفاده میشود -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- کامپوننت مورد استفاده توسط Spring RestTemplate -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
</dependencies>
</project>
- خطوط ۱۵–۲۰: پروژه والد Spring Boot؛
- خطوط ۲۴–۲۷: کلاینت کنسول وبسرور / jSON بر اساس کامپوننتی به نام [RestTemplate] که توسط وابستگی [spring-web] ارائه میشود، ساخته شده است؛
- خطوط ۲۹–۳۶: سریالیسازی/دسریالیسازی اشیاء jSON نیازمند کتابخانه jSON است. ما از یک نسخه از کتابخانه Jackson که در Spring Web استفاده میشود، بهره میبریم؛
- خطوط ۳۸–۴۱: در پایینترین سطح، کامپوننت [RestTemplate] از طریق ساکتهای TCP/IP با سرور ارتباط برقرار میکند. ما میخواهیم مقدار [timeout] را برای این موارد تنظیم کنیم، یعنی حداکثر زمان انتظار برای دریافت پاسخ از سرور. کامپوننت [RestTemplate] به ما اجازه نمیدهد این مقدار را تنظیم کنیم. برای این کار، یک کامپوننت سطح پایین ارائهشده توسط وابستگی [org.apache.httpcomponents.httpclient] را به کانتراکتور [RestTemplate] پاس میکنیم. این وابستگی است که به ما اجازه میدهد [timeout] را برای ارتباط تنظیم کنیم؛
8.5.3. پکیج [rdvmedecins.client.entities]
![]() |
پکیج [rdvmedecins.client.entities] تمام اِنتِتیهایی را که سرویس وب / jSON از طریق URLهای مختلف خود ارسال میکند، گرد هم میآورد. ما دیگر به تفصیل در مورد آنها بحث نخواهیم کرد. کافی است بگوییم که تمام انوتیشنهای JPA و همچنین انوتیشنهای jSON از این دو انتیتy، یعنی JPA و [Client, Creneau, Medecin, Rv, Personne]، حذف شدهاند. در اینجا، بهعنوان مثال، کلاس [Rv] را میبینید:
package rdvmedecins.client.entities;
import java.util.Date;
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// تاریخ قرار ملاقات
private Date jour;
//یک قرار ملاقات به یک مشتری مرتبط است
private Client client;
//یک قرار ملاقات به یک بازه زمانی متصل است
private Creneau creneau;
// کلیدهای خارجی
private long idClient;
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);
}
// گیرندهها و تنظیمکنندهها
...
}
8.5.4. پکیج [rdvmedecins.client.requests]
![]() |
پکیج [rdvmedecins.client.requests] دو کلاس را ترکیب میکند که مقدار آن (jSON) به URL، [/ajouterRv] و [supprimerRv] ارسال میشود. آنها با معادلهای سمت سرور خود یکسان هستند.
8.5.5. پکیج [rdvmedecins.client.responses]
![]() |
[Response] نوع تمام پاسخهای سرویس وب / jSON است. این یک نوع عمومی است:
package rdvmedecins.client.responses;
import java.util.List;
public class Response<T> {
// ----------------- ویژگیها
//وضعیت عملیات
private int status;
// هر پیام خطا
private List<String> messages;
// بدنه پاسخ
private T body;
// سازندهها
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// گیرنده و تنظیمکننده
...
}
- خط ۵: نوع [T] بسته به URL سرویس وب / jSON متغیر است؛
8.5.6. بسته [rdvmedecins.client.dao]
![]() |
- [IDao] رابط لایه [DAO] است و [Dao] پیادهسازی آن میباشد. بعداً به این پیادهسازی باز خواهیم گشت؛
8.5.7. پکیج [rdvmedecins.client.config]
![]() |
کلاس [DaoConfig] برنامه را پیکربندی میکند. کد آن به شرح زیر است:
package rdvmedecins.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@Configuration
@ComponentScan({ "rdvmedecins.client.dao" })
public class DaoConfig {
@Bean
public RestTemplate restTemplate() {
// ایجاد کامپوننت RestTemplate
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// نتیجه
return restTemplate;
}
// نقشهبرداران jSON
@Bean
public ObjectMapper jsonMapper(){
return new ObjectMapper();
}
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",
creneauFilter));
return jsonMapperLongRv;
}
@Bean
public ObjectMapper jsonMapperShortRv() {
ObjectMapper jsonMapperShortRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
return jsonMapperShortRv;
}
}
- خط ۱۳: کلاس [DaoConfig] یک کلاس پیکربندی Spring است؛
- خط ۱۴: برای یافتن کامپوننتهای Spring، در پکیج [rdvmedecins.client.dao] جستجو خواهد شد. کامپوننت [Dao] در آنجا یافت میشود؛
- خطوط 17–24: یک سینگلتون Spring به نام [restTemplate] (نام متد) تعریف کنید. این متد یک نمونه از `[RestTemplate]` را بازمیگرداند، که ابزار پایهای است که Spring برای ارتباط با یک سرویس وب (`jSON`) فراهم میکند؛
- خط ۲۱: میتوانیم [RestTemplate restTemplate = new RestTemplate() ;] را بنویسیم. این در بیشتر موارد کافی است. با این حال، در اینجا میخواهیم [timeout] مشتری را تنظیم کنیم. برای انجام این کار، ما یک کامپوننت سطح پایین از نوع [HttpComponentsClientHttpRequestFactory] (خط ۲۰) را به کامپوننت [RestTemplate] تزریق میکنیم، که به ما اجازه میدهد این مقادیر [timeout] را تنظیم کنیم. وابستگی لازم Maven فراهم شده است؛
- خطوط 28–57: تعریف مپکنندههای jSON. اینها مپرهای jSON هستند که در سمت سرور (به بخش 8.4.11.3 مراجعه کنید) برای سریالیزه کردن نوع T در پاسخ [Response<T>] استفاده میشوند. همین مبدلها اکنون در سمت کلاینت برای دسریالیزه کردن نوع T استفاده خواهند شد؛
8.5.8. رابط [IDao]
بیایید به معماری برنامه بازگردیم:
![]() |
لایه [DAO] به عنوان یک مبدل بین لایه [console] و رابطهای URL که توسط سرویس وب /jSON ارائه شدهاند، عمل میکند. رابط آن، [IDao]، به شرح زیر خواهد بود:
package rdvmedecins.client.dao;
import java.util.List;
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
public interface IDao {
// URL سرویس وب
public void setUrlServiceWebJson(String url);
// timeout
public void setTimeout(int timeout);
// احراز هویت
public void authenticate(User user);
// فهرست مشتریان
public List<Client> getAllClients(User user);
// فهرست پزشکان
public List<Medecin> getAllMedecins(User user);
// فهرست شکافهای نوبتدهی پزشک
public List<Creneau> getAllCreneaux(User user, long idMedecin);
// پیدا کردن مشتری با شناسهٔ آن
public Client getClientById(User user, long id);
// پیدا کردن یک مشتری بر اساس شناسه آن
public Medecin getMedecinById(User user, long id);
// پیدا کردن یک قرار ملاقات بر اساس شناسه آن
public Rv getRvById(User user, long id);
// پیدا کردن یک نوبت ملاقات با شناسهٔ آن
public Creneau getCreneauById(User user, long id);
//یک RV اضافه کنید
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient);
// حذف یک RV
public void supprimerRv(User user, long idRv);
// فهرست قرارهای ملاقات پزشک در یک روز معین
public List<Rv> getRvMedecinJour(User user, long idMedecin, String jour);
//دفترچه یادداشت
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);
}
- خط ۱۴: متد برای تنظیم ریشه URL سرویس وب / jSON، برای مثال [http://localhost:8080];
- خط 17: متد تنظیم سمت-کلاینت [timeout]. ما میخواهیم این پارامتر را کنترل کنیم زیرا برخی از کلاینتهای HTTP گاهی اوقات ممکن است مدت زمان بسیار طولانی منتظر پاسخی باشند که هرگز نخواهد آمد؛
- خط ۲۰: متد شناسایی کاربر [login, passwd]. در صورتی که کاربر شناسایی نشود، یک استثنا (exception) پرتاب میکند؛
- خطوط 22–53: هر URL که توسط سرویس وب / jSON ارائه شده است، با یک متد از اینفراساخت مرتبط است که امضای آن از امضای متد سمت سرور که URL ارائه شده را مدیریت میکند، استخراج شده است. برای مثال، URL سمت سرور زیر را در نظر بگیرید:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Response<String> getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
- خط ۱: میبینیم که [idMedecin] و [jour] پارامترهای URL هستند. اینها پارامترهای ورودی برای متد مرتبط با این URL سمت-کلاینت خواهند بود؛
- خط ۲: میبینیم که متد سمت سرور نوع [Response<String>] را برمیگرداند. این نوع، [String]، نوع مقدار jSON است که از نوع [AgendaMedecinJour] میباشد. نوع نتیجه متد مرتبط با این URL در سمت کلاینت، [AgendaMedecinJour] خواهد بود؛
در سمت کلاینت، متد زیر اعلام شده است:
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);
این امضا زمانی مناسب است که سرور یک پاسخ [int status, List<String> messages, String body] را با [status0] ارسال کند. در این حالت، ما [messagesnull && body!=null] را داریم. این امضا زمانی که [status!=0] باشد، مناسب نیست. در این حالت، ما [messages!=null && body==null] را داریم. باید به نحوی نشان دهیم که خطایی رخ داده است. برای این کار، یک استثنا از نوع [RdvMedecinsException] را به شرح زیر پرتاب خواهیم کرد:
package rdvmedecins.client.dao;
import java.util.List;
public class RdvMedecinsException extends RuntimeException {
private static final long serialVersionUID = 1L;
// کد خطا
private int status;
// فهرست پیامهای خطا
private List<String> messages;
public RdvMedecinsException() {
}
public RdvMedecinsException(int code, List<String> messages) {
super();
this.status = code;
this.messages = messages;
}
// گیرنده و تنظیمکننده
...
}
- خطوط ۹ و ۱۱: استثنا مقادیر فیلدهای [status, messages] را از شیء [Response<T>] که توسط سرور ارسال شده است، دریافت خواهد کرد؛
- خط ۵: کلاس [RdvMedecinsException] از کلاس [RuntimeException] ارث میبرد. بنابراین این یک استثنای مدیریتنشده است، یعنی نیازی به مدیریت آن با یک بلوک try/catch یا اعلام آن در امضاهای متد اینفترفیس نیست؛
علاوه بر این، تمام متدهای رابط [IDao] که سرویس وب /jSON را استعلام میکنند، نوع زیر [User] را به عنوان پارامتر میگیرند:
package rdvmedecins.client.entities;
public class User {
// دادهها
private String login;
private String passwd;
// سازندهها
public User() {
}
public User(String login, String passwd) {
this.login = login;
this.passwd = passwd;
}
// گیرنده و تنظیمکننده
...
}
در واقع، هر تبادل با سرویس وب /jSON باید با یک هدر احراز هویت HTTP همراه باشد.
8.5.9. پکیج [rdvmedecins.clients.console]
اکنون که با رابط لایه [DAO] آشنا شدهایم، میتوانیم برنامه کنسول را ارائه دهیم.
![]() |
کلاس [Main] به شرح زیر است:
package rdvmedecins.clients.console;
import java.io.IOException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
//سریالیساز jSON
static private ObjectMapper mapper = new ObjectMapper();
// زمانانتظار اتصال به میلیثانیه
static private int TIMEOUT = 1000;
public static void main(String[] args) throws IOException {
// یک مرجع از لایه [DAO] بازیابی میشود
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
IDao dao = context.getBean(IDao.class);
// تنظیم URL برای سرویس وب / JSON
dao.setUrlServiceWebJson("http://localhost:8080");
// تنظیم زمانبندیها به میلیثانیه
dao.setTimeout(TIMEOUT);
//احراز هویت
String message = "/authenticate [admin,admin]";
try {
dao.authenticate(new User("admin", "admin"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [user,user]";
try {
dao.authenticate(new User("user", "user"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [user,x]";
try {
dao.authenticate(new User("user", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [x,x]";
try {
dao.authenticate(new User("x", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [admin,x]";
try {
dao.authenticate(new User("admin", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// فهرست مشتریان
message = "/getAllClients";
try {
showResponse(message, dao.getAllClients(new User("admin", "admin")));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// فهرست پزشکان
message = "/getAllMedecins";
try {
showResponse(message, dao.getAllMedecins(new User("admin", "admin")));
} catch (RdvMedecinsException e) {
showException(message, e);
}
//فهرست شکافهای زمانی نوبتدهی دکتر ۲
message = "/getAllCreneaux/2";
try {
showResponse(message, dao.getAllCreneaux(new User("admin", "admin"), 2L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// شمارهٔ مشتری ۱
message = "/getClientById/1";
try {
showResponse(message, dao.getClientById(new User("admin", "admin"), 1L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// پزشک شمارهٔ ۲
message = "/getMedecinById/2";
try {
showResponse(message, dao.getMedecinById(new User("admin", "admin"), 2L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// اسلات نوبت شمارهٔ ۳
message = "/getCreneauById/3";
try {
showResponse(message, dao.getCreneauById(new User("admin", "admin"), 3L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// قرار ملاقات شماره ۴
message = "/getRvById/4";
try {
showResponse(message, dao.getRvById(new User("admin", "admin"), 4L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// افزودن یک قرار ملاقات
message = "/AjouterRv [idClient=4,idCreneau=8,jour=2015-01-08]";
long idRv = 0;
try {
Rv response = dao.ajouterRv(new User("admin", "admin"), "2015-01-08", 8L, 4L);
idRv = response.getId();
showResponse(message, response);
} catch (RdvMedecinsException e) {
showException(message, e);
}
// فهرست قرارهای دکتر ۱ در ۸ ژانویه ۲۰۱۵
message = "/getRvMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
//دفترچه یادداشت دکتر ۱ در تاریخ ۸ ژانویه ۲۰۱۵
message = "/getAgendaMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getAgendaMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
//حذف قرار ملاقات اضافه شده
message = String.format("/supprimerRv [idRv=%s]", idRv);
try {
dao.supprimerRv(new User("admin", "admin"), idRv);
} catch (RdvMedecinsException e) {
showException(message, e);
}
//فهرست قرارهای دکتر ۱ در ۸ ژانویه ۲۰۱۵
message = "/getRvMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// بستن زمینه
context.close();
}
private static void showException(String message, RdvMedecinsException e) {
System.out.println(String.format("URL [%s]", message));
System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
for (String msg : e.getMessages()) {
System.out.println(msg);
}
}
private static <T> void showResponse(String message, T response) throws JsonProcessingException {
System.out.println(String.format("URL [%s]", message));
System.out.println(mapper.writeValueAsString(response));
}
}
- خط ۱۹: سریالیساز jSON که به ما امکان نمایش پاسخ سرور را میدهد، خط ۱۸۴؛
- خط 25: کامپوننت [AnnotationConfigApplicationContext] یک کامپوننت Spring است که قادر به استفاده از anotationهای پیکربندی یک برنامه Spring میباشد. ما کلاس [AppConfig] را که برنامه را پیکربندی میکند، به سازندهاش پاس میدهیم؛
- خط ۲۶: ما یک مرجع به لایه [DAO] را بازیابی میکنیم؛
- خطوط ۲۷–۳۰: ما آن را پیکربندی میکنیم؛
- خطوط ۳۲–۱۶۹: ما تمام متدهای رابط [IDao] را تست میکنیم؛
نتایج بهدستآمده به شرح زیر است:
09:20:56.935 [main] INFO o.s.c.a.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@52feb982: startup date [Wed Oct 14 09:20:56 CEST 2015]; root of context hierarchy
/authenticate [admin,admin] : OK
URL [/authenticate [user,user]]
L'erreur n° [111] s'est produite :
403 Forbidden
URL [/authenticate [user,x]]
L'erreur n° [111] s'est produite :
401 Unauthorized
URL [/authenticate [x,x]]
L'erreur n° [111] s'est produite :
403 Forbidden
URL [/authenticate [admin,x]]
L'erreur n° [111] s'est produite :
401 Unauthorized
URL [/getAllClients]
[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]
URL [/getAllMedecins]
[{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"},{"id":3,"version":1,"titre":"Mr","nom":"JANDOT","prenom":"Philippe"},{"id":4,"version":1,"titre":"Melle","nom":"JACQUEMOT","prenom":"Justine"}]
URL [/getAllCreneaux/2]
[{"id":25,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"medecin":null,"idMedecin":2},{"id":26,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"medecin":null,"idMedecin":2},{"id":27,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":2},{"id":28,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"medecin":null,"idMedecin":2},{"id":29,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"medecin":null,"idMedecin":2},{"id":30,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"medecin":null,"idMedecin":2},{"id":31,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"medecin":null,"idMedecin":2},{"id":32,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":2},{"id":33,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"medecin":null,"idMedecin":2},{"id":34,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"medecin":null,"idMedecin":2},{"id":35,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"medecin":null,"idMedecin":2},{"id":36,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"medecin":null,"idMedecin":2}]
URL [/getClientById/1]
{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"}
URL [/getMedecinById/2]
{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"}
URL [/getCreneauById/3]
{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":1}
URL [/getRvById/4]
L'erreur n° [2] s'est produite :
Le rendez-vous d'id [4] n'existe pas
URL [/ajouterRv [idClient=4,idCreneau=8,jour=2015-01-08]]
{"id":144,"version":0,"jour":1420671600000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":0,"idCreneau":0}
URL [/getRvMedecinJour/1/2015-01-08]
[{"id":144,"version":0,"jour":1420675200000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":4,"idCreneau":8}]
URL [/getAgendaMedecinJour/1/2015-01-08]
{"medecin":{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},"jour":1420671600000,"creneauxMedecinJour":[{"creneau":{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":2,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":4,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":5,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":6,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":7,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"rv":{"id":144,"version":0,"jour":1420675200000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":4,"idCreneau":8}},{"creneau":{"id":9,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":10,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":11,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":12,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":13,"version":1,"hdebut":14,"mdebut":0,"hfin":14,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":14,"version":1,"hdebut":14,"mdebut":20,"hfin":14,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":15,"version":1,"hdebut":14,"mdebut":40,"hfin":15,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":16,"version":1,"hdebut":15,"mdebut":0,"hfin":15,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":17,"version":1,"hdebut":15,"mdebut":20,"hfin":15,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":18,"version":1,"hdebut":15,"mdebut":40,"hfin":16,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":19,"version":1,"hdebut":16,"mdebut":0,"hfin":16,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":20,"version":1,"hdebut":16,"mdebut":20,"hfin":16,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":21,"version":1,"hdebut":16,"mdebut":40,"hfin":17,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":22,"version":1,"hdebut":17,"mdebut":0,"hfin":17,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":23,"version":1,"hdebut":17,"mdebut":20,"hfin":17,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":24,"version":1,"hdebut":17,"mdebut":40,"hfin":18,"mfin":0,"medecin":null,"idMedecin":1},"rv":null}]}
URL [/getRvMedecinJour/1/2015-01-08]
[]
09:21:00.258 [main] INFO o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@52feb982: startup date [Wed Oct 14 09:20:56 CEST 2015]; root of context hierarchy
به خواننده واگذار میکنیم که نتایج را با کد مطابقت دهد. کد نشان میدهد چگونه هر متد لایه [DAO] را فراخوانی کنیم. بیایید به چند نکته اشاره کنیم:
- خطوط ۲–۱۴: نشان میدهند که در صورت خطای احراز هویت، سرور بسته به شرایط، وضعیت HTTP، [403 Forbidden] یا [401 Unauthorized] را برمیگرداند؛
- خطوط ۳۰–۳۱: یک قرار ملاقات (Rv) برای پزشک شماره ۱ اضافه میشود؛
- خطوط ۳۲–۳۳: این قرار ملاقات نمایش داده میشود. این تنها قرار ملاقات آن روز است؛
- خطوط ۳۴–۳۵: این قرار در دفترچه یادداشت پزشک نیز ظاهر میشود؛
- خطوط ۳۶–۳۷: قرار ملاقات ناپدید شده است. کد در این فاصله آن را حذف کرده است؛
لاگهای کنسول توسط فایلهای زیر کنترل میشوند:
![]() |
[application.properties]
logging.level.org.springframework.web=OFF
logging.level.org.hibernate=OFF
spring.main.show-banner=false
logging.level.httpclient.wire=OFF
[logback.xml]
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- کدگذارها بهطور پیشفرض نوع ch.qos.logback.classic.encoder.PatternLayoutEncoder را دریافت میکنند. -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!--کنترل سطح لاگ -->
<root level="info"> <!--خاموش، اطلاعات، اشکالزدایی، هشدار -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
8.5.10. پیادهسازی لایه [DAO]
اکنون باید هسته لایه [DAO] را ارائه دهیم: پیادهسازی رابط آن، [IDao]. این کار را گام به گام انجام خواهیم داد.
![]() |
رابط [IDao] توسط کلاس انتزاعی [AbstractDao] و کلاس فرزند آن [Dao] پیادهسازی شده است.
کلاس والد [AbstractDao] به شرح زیر است:
package rdvmedecins.client.dao;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.RequestEntity.BodyBuilder;
import org.springframework.http.RequestEntity.HeadersBuilder;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import rdvmedecins.client.entities.User;
public abstract class AbstractDao implements IDao {
// دادهها
@Autowired
protected RestTemplate restTemplate;
protected String urlServiceWebJson;
// URL سرویس وب / jSON
public void setUrlServiceWebJson(String url) {
this.urlServiceWebJson = url;
}
public void setTimeout(int timeout) {
// تنظیم محدودیت زمانی برای درخواستهای کلاینت وب
HttpComponentsClientHttpRequestFactory factory = (HttpComponentsClientHttpRequestFactory) restTemplate
.getRequestFactory();
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
}
private String getBase64(User user) {
// رمزگذاری Base64 نام کاربری و رمز عبور – نیاز دارد
// جاوا ۸
String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
}
// درخواست عمومی
protected String getResponse(User user, String url, String jsonPost) {
...
}
}
- خط ۲۰: این کلاس انتزاعی است، که مانع از تعیین آن به عنوان یک کامپوننت Spring میشود. این کلاس فرزند آن است که به این عنوان تعیین خواهد شد؛
- خطوط ۲۳–۲۴: ما بیون [restTemplate] را تزریق میکنیم، که آن را در کلاس پیکربندی [AppConfig] تعریف کردهایم؛
- خط ۲۵: ریشه URL سرویس وب / jSON;
- خطوط ۳۲–۳۸: تنظیم تایماوت کلاینت در حین انتظار برای پاسخ از سرور؛
- خط ۳۴: ما کامپوننت [HttpComponentsClientHttpRequestFactory] را که هنگام ایجاد bean [restTemplate] به آن تزریق کرده بودیم، بازیابی میکنیم (به [AppConfig] مراجعه کنید);
- خط ۳۶: ما حداکثر زمان انتظار برای مشتری را در حین برقراری ارتباط با سرور تنظیم میکنیم؛
- خط ۳۷: ما حداکثر زمان انتظار را برای کلاینت در حالی که منتظر پاسخ یکی از درخواستهای خود است، تنظیم میکنیم؛
پیادهسازی روشهای ارتباط با سرور در روش عمومی زیر لحاظ خواهد شد:
// درخواست عمومی
protected String getResponse(User user, String url, String jsonPost) {
...
}
- خط ۲: پارامترهای [getResponse] به شرح زیر است:
- [User user]: کاربری که اتصال را برقرار میکند؛
- [String url]: URL مورد پرسوجو. این پایان URL است، که بخش اول آن توسط فیلد [urlServiceWebJson] کلاس فراهم میشود،
- [String jsonPost]: رشته jSON برای ارسال. اگر این مقدار موجود باشد، URL با یک POST درخواست خواهد شد؛ در غیر این صورت، با یک GET درخواست خواهد شد؛
ادامه میدهیم:
// درخواست عمومی
protected String getResponse(User user, String url, String jsonPost) {
// URL: URL برای تماس
//jsonPost: مقدار jSON برای ارسال
try {
// اجرای درخواست
RequestEntity<?> request;
if (jsonPost == null) {
HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url))).accept(MediaType.APPLICATION_JSON);
if (user != null) {
headersBuilder = headersBuilder.header("Authorization", getBase64(user));
}
request = headersBuilder.build();
} else {
BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
if (user != null) {
bodyBuilder = bodyBuilder.header("Authorization", getBase64(user));
}
request = bodyBuilder.body(jsonPost);
}
// اجرای درخواست
return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
}).getBody();
} catch (URISyntaxException e) {
throw new RdvMedecinsException(20, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(21, getMessagesForException(e));
}
}
- خطوط ۲۳–۲۴: دستوری که درخواست را به سرور ارسال میکند و پاسخ آن را دریافت میکند. کامپوننت [RestTemplate] طیف گستردهای از روشها را برای ارتباط با سرور ارائه میدهد. میتوانستیم روش دیگری به جز [exchange] انتخاب کنیم. پارامتر دوم فراخوانی نوع پاسخ مورد انتظار را مشخص میکند، در این مورد یک رشته از نوع jSON. پارامتر اول درخواست از نوع [RequestEntity] است (خط ۷). نتیجه متد [exchange] از نوع [ResponseEntity<String>] است. نوع [ResponseEntity] پاسخ کامل سرور را در بر میگیرد که شامل سربرگهای HTTP و سند ارسالشده توسط سرور است. به همین ترتیب، نوع [RequestEntity] کل درخواست کلاینت را در بر میگیرد، شامل سربرگهای HTTP و هرگونه دادهی ارسالشده؛
- خط ۲۳: این بدنهٔ شیء [ResponseEntity<String>] است که به متد فراخوانیکننده بازگردانده میشود، یعنی رشتهٔ jSON که توسط سرور ارسال شده است؛
- خطوط ۹–۲۱: ما باید درخواست از نوع [RequestEntity] را بسازیم. این امر بسته به اینکه از GET یا POST برای ارسال درخواست استفاده شود، متفاوت است؛
- خط ۹: درخواست برای GET. کلاس [RequestEntity] متدهای ایستا برای ایجاد پرسوجوهای GET، POST، HEAD و غیره را فراهم میکند. روش [RequestEntity.get] به شما امکان میدهد تا با زنجیرهسازی روشهای مختلفی که آن را میسازند، یک پرسوجوی GET ایجاد کنید:
- متد [RequestEntity.get] هدف URL را به صورت یک نمونه از URI به عنوان پارامتر میپذیرد،
- متد [accept] به شما امکان میدهد عناصر هدر HTTP را تعریف کنید. در اینجا، مشخص میکنیم که نوع [application/json] را که سرور ارسال خواهد کرد، میپذیریم؛
- نتیجهٔ این زنجیرهسازی متد، نوع [HeadersBuilder] است؛
- خطوط ۱۰–۱۲: اگر پارامتر [User user] برابر null نباشد، ما هدرهای HTTP و [Authorization] را در درخواست قرار میدهیم؛
- خط ۱۳: متد [HeadersBuilder.build] از این اطلاعات مختلف برای ساخت نوع پرسوجوی [RequestEntity] استفاده میکند؛
- خط ۱۵: درخواست برای POST. روش [RequestEntity.post] اجازه میدهد تا یک درخواست POST با زنجیرهسازی روشهای مختلف که آن را میسازند، ایجاد شود:
- متد [RequestEntity.post]، هدف URL را به صورت یک نمونه از URI به عنوان پارامتر میپذیرد،
- روش [header] به شما امکان میدهد تا سربرگهای HTTP مورد نظر خود را تعریف کنید؛ در این مورد، سربرگ احراز هویت،
- متد زیر، [header]، هدر [Content-Type: application/json] را در درخواست قرار میدهد تا نشان دهد که مقدار ارسالشده به صورت یک رشته jSON دریافت خواهد شد؛
- روش [accept] به ما امکان میدهد مشخص کنیم که نوع [application/json] را که سرور ارسال خواهد کرد، میپذیریم؛
- خطوط 17–19: اگر پارامتر [User user] برابر null نباشد، ما هدر HTTP [Authorization] را در درخواست قرار میدهیم؛
- خط ۲۰: متد [BodyBuilder.body] مقدار ارسالشده را تعیین میکند. این پارامتر دوم متد عمومی [getResponse] (خط ۲) است؛
- خطوط ۲۵–۲۸: اگر هرگونه خطا رخ دهد، یک استثنای [RdvMedecinsException] پرتاب میشود؛
متد [getMessagesForException] در خطوط 26 و 28 به شرح زیر است:
// فهرست پیامهای خطا برای یک استثنا
protected static List<String> getMessagesForException(Exception exception) {
//بازیابی فهرست پیامهای خطا برای استثنا
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
// پیام را فقط در صورتی بازیابی میکند که !=null و خالی نباشد
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// علت بعدی
cause = cause.getCause();
}
return erreurs;
}
متد خصوصی [getBase64] رشته Base64 رمزگذاریشده 'login:passwd' را برای هدر احراز هویت HTTP فراهم میکند:
private String getBase64(User user) {
// رمزگذاری کاربر و رمز عبور او در Base64 – نیازمند Java 8
String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
}
کلاس [Dao] کلاس [AbstractDao] را به شرح زیر گسترش میدهد:
package rdvmedecins.client.dao;
import java.io.IOException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
import rdvmedecins.client.requests.PostAjouterRv;
import rdvmedecins.client.requests.PostSupprimerRv;
import rdvmedecins.client.responses.Response;
@Service
public class Dao extends AbstractDao implements IDao {
// jSON نگاشتکنندهها
@Autowired
ObjectMapper jsonMapper;
@Autowired
private ObjectMapper jsonMapperShortCreneau;
@Autowired
private ObjectMapper jsonMapperLongRv;
@Autowired
private ObjectMapper jsonMapperShortRv;
public List<Client> getAllClients(User user) {
...
}
public List<Medecin> getAllMedecins(User user) {
...
}
...
}
- خط ۲۲: کلاس [Dao] یک کامپوننت Spring است. آنوتیشن [@Service] در اینجا استفاده شده است. میتوانستیم به استفاده از آنوتیشن [@Component] که تا این نقطه استفاده شده بود، ادامه دهیم؛
- خطوط ۲۶–۳۶: تزریق چهار مپر jSON که در کلاس پیکربندی [DaoConfig] تعریف شدهاند؛
متدهای کلاس [Dao] همگی از الگوی یکسانی پیروی میکنند. ما یک عملیات GET و یک عملیات POST را به تفصیل توضیح خواهیم داد.
ابتدا، یک پرسوجو [GET]:
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour) {
// پاسخ
Response<AgendaMedecinJour> response;
// تقویم
String jsonResponse = getResponse(user, String.format("%s/%s/%s", "/getAgendaMedecinJour", idMedecin, jour), null);
try {
// تقویم AgendaMedecinJour
response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<AgendaMedecinJour>>() {
});
} catch (IOException e) {
throw new RdvMedecinsException(401, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(402, getMessagesForException(e));
}
// تحلیل پاسخ
int status = response.getStatus();
if (status != 0) {
throw new RdvMedecinsException(status, response.getMessages());
} else {
return response.getBody();
}
}
- خط ۵: متد عمومی [getResponse] فراخوانی میشود. پارامترهای واقعی مورد استفاده به شرح زیر هستند:
- 1: کاربر؛
- ۲: هدف URL;
- ۳: مقداری که باید ارسال شود. در این مورد، هیچ مقداری وجود ندارد؛
- خط ۵: فراخوانی در داخل بلوک try/catch قرار نگرفته است. ممکن است متد [getResponse] یک استثنای [RdvMedecinsException] را پرتاب کند. اگر این استثنا پرتاب شود، این خطا به متدی که متد [getAgendaMedecinJour] را فراخوانی کرده است، بازگردانده خواهد شد؛
- خط ۸: theURL [/getAgendaMedecinJour] یک شیء [Response<AgendaMedecinJour>] را ارسال میکند که توسط mapper jSON QZX در سمت سرور به jSON سریالیزه شده است.W2HTMLP005748ZQX. همین مپر برای سریالمعکوسسازی رشته دریافتی jSON استفاده میشود؛
- خطوط ۱۰–۱۳: اگر خطایی در خط ۹ رخ دهد، یک استثنای [RdvMedecinsException] پرتاب میشود؛
- خطوط ۱۶–۲۱: پاسخ ارسالشده توسط سرور تحلیل میشود؛
- خطوط ۱۷–۱۸: اگر سرور خطایی گزارش کرده باشد، یک استثنا پرتاب میشود که حاوی اطلاعاتی است که توسط سرور ارسال شده است؛
- خطوط ۱۹–۲۱: در غیر این صورت، دفترچه یادداشت پزشک بازگردانده میشود؛
درخواست POST مورد بررسی به شرح زیر خواهد بود:
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
// پاسخ
Response<Rv> response;
try {
// قرار ملاقات
String jsonResponse = getResponse(user, "/ajouterRv",
jsonMapper.writeValueAsString(new PostAjouterRv(idClient, idCreneau, jour)));
// قرار ملاقات قرار ملاقات
response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<Rv>>() {
});
} catch (RdvMedecinsException e) {
throw e;
} catch (IOException e) {
throw new RdvMedecinsException(381, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(382, getMessagesForException(e));
}
// تحلیل پاسخ
int status = response.getStatus();
if (status != 0) {
throw new RdvMedecinsException(status, response.getMessages());
} else {
return response.getBody();
}
}
- خط ۶: متد [getResponse] با پارامترهای زیر فراخوانی میشود:
- 1: کاربر؛
- ۲: هدف URL،
- ۳: مقدار ارسالشده: مقدار jSON از نوع [PostAjouter]، که با استفاده از اطلاعات دریافتشده بهعنوان پارامتر توسط متد ساخته شده است، ارسال میشود. از یک مپر jSON بدون فیلتر استفاده میشود؛
- خط ۹: در سمت سرور، این مپر jSON [jsonMapperLongRv] است که پاسخ سرور را سریالیزه میکند. در سمت کلاینت، از همین مپر برای دسریالیزه کردن آن استفاده میشود؛
- خط ۶: مپر URL [/ajouterRv] مقدار jSON از نوع [Response<Rv>] را برمیگرداند؛
- خطوط ۴–۱۱: در اینجا، متد [getResponse] در داخل یک بلوک try/catch قرار داده شده است زیرا سریالسازی مقدار ارسالشده ممکن است یک استثنا ایجاد کند. متد [getResponse] ممکن است یک استثنای [RdvMedecinsException] را پرتاب کند. در این صورت، ما به سادگی آن را دوباره اجرا میکنیم (خطوط ۱۱–۱۲)؛
کد زیر (خطوط ۱۳–۲۴) مشابه مورد قبلی است. بنابراین تنها تفاوت در یک عملیات GET، پارامتر دوم متد [getResponse] است که باید مقدار jSON از مقداری باشد که قرار است ارسال شود.
سایر متدها به همین شکل ساختار یافتهاند.
8.5.11. ناهنجاری
در طول آزمایشهای مختلف، یک ناهنجاری مشاهده شد که در کلاس زیر [Anomalie] خلاصه شده است:
package rdvmedecins.clients.console;
import java.io.IOException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Anomalie {
//سریالیکننده jSON
static private ObjectMapper mapper = new ObjectMapper();
// زمانانتظار اتصال به میلیثانیه
static private int TIMEOUT = 1000;
public static void main(String[] args) throws IOException {
// یک مرجع از لایه [DAO] بازیابی میشود
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
IDao dao = context.getBean(IDao.class);
// تنظیم URL برای سرویس وب / JSON
dao.setUrlServiceWebJson("http://localhost:8080");
//تنظیم زمانبندیها به میلیثانیه
dao.setTimeout(TIMEOUT);
//احراز هویت
String message = "/authenticate [admin,admin]";
try {
dao.authenticate(new User("admin", "admin"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
//احراز هویت
message = "/authenticate [admin,x]";
try {
dao.authenticate(new User("admin", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// احراز هویت
message = "/authenticate [user,user]";
try {
dao.authenticate(new User("user", "user"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// بستن زمینه
context.close();
}
private static void showException(String message, RdvMedecinsException e) {
System.out.println(String.format("URL [%s]", message));
System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
for (String msg : e.getMessages()) {
System.out.println(msg);
}
}
}
- خطوط ۳۱–۳۸: کاربر [admin, admin] احراز هویت شده است؛
- خطوط ۴۰–۴۷: کاربر [admin, x] احراز هویت شده است، اما رمز عبور او نادرست است؛
- خطوط ۴۹–۵۶: کاربر [user, user] احراز هویت شده است؛ این کاربر وجود دارد اما مجاز نیست؛
نتایج به شرح زیر است:
- خط ۲: برخلاف انتظارات، کاربر [admin, x] پذیرفته شد؛
اگر خطوط ۳۳–۳۸ کد را غیرفعال کنیم، نتیجه زیر را میگیریم:
که نتیجه مورد انتظار است. گویی به محض اینکه کاربر [admin, admin] برای اولین بار با موفقیت وارد سیستم شد، رمز عبور او برای ورودهای بعدی دیگر مورد نیاز نیست. و این دقیقاً همان چیزی است که اتفاق میافتد. به طور پیشفرض، Spring Security از یک جلسه (session) استفاده میکند، به این معنی که پس از احراز هویت کاربر، در درخواستهای بعدی نیازی به انجام مجدد آن نیست. ما میتوانیم پیکربندی [Spring Security] را در سرور وب / jSON تغییر دهیم تا این وضعیت دیگر برقرار نباشد:
![]() |
فایل [SecurityConfig] باید به شرح زیر اصلاح شود:
@Override
protected void configure(HttpSecurity http) throws Exception {
...
// هیچ جلسهای وجود ندارد
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
- خط ۵ مشخص میکند که نباید هیچ جلسه امنیتی وجود داشته باشد؛
این مشکل را حل کرده است.
8.6. کد سمت سرور Spring / Thymeleaf
8.6.1. مقدمه
بیایید به معماری برنامهٔ کلاینت/سرور مورد نظر برای ساخت بازگردیم:
![]() |
- سرور وب [Web2] / jSON ساخته شده است؛
- لایهٔ کلاینت [DAO] از کلاینت [Web1] ساخته شده است؛
رابطه بین سرور [Web1] و مرورگرهای کلاینت یک رابطه کلاینت–سرور است، که در آن سرور یک وبسرور / jSON است. در واقع، [Web1] جریانهای HTML را که در یک رشته jSON جای گرفتهاند، تحویل میدهد. معماری کلاینت/سرور به شرح زیر است:
![]() |
- ما یک معماری مشتری-سرور (مشتری: [2] / سرور: [1]) داریم که در آن مشتری و سرور از طریق jSON با یکدیگر ارتباط برقرار میکنند؛
- در [1]، لایه وب Spring MVC / Thymeleaf نماها، قطعات نما و دادهها را در jSON ارائه میدهد. بنابراین سرور یک وب سرور / jSON است، درست مانند سرور [Web1]. این سرور همچنین بدون حالت است؛
- در [2]: کد جاوااسکریپتی که در نمای بارگذاریشده هنگام راهاندازی برنامه تعبیه شده است، بهصورت لایهای ساختار یافته است:
- لایه [présentation] تعاملات کاربر را مدیریت میکند،
- لایه [DAO] دسترسی به دادهها را از طریق سرور [Web2] مدیریت میکند؛
- کلاینت [2] برخی از نماها را برای کاهش بار روی سرور کش میکند؛
ما وبسرور / jSON [Web1] را که با استفاده از Spring MVC / Thymeleaf پیادهسازی شده است، در چند مرحله خواهیم ساخت:
- کاوش در چارچوب CSS Bootstrap؛
- نوشتن ویوها؛
- نوشتن کنترلر؛
سپس، به طور جداگانه، کلاینت JS را برای سرور [Web1] خواهیم ساخت. برای نشان دادن واضح اینکه این کلاینت تا حدی از سرور [Web1] مستقل است، آن را با استفاده از ابزار [Webstorm] به جای STS خواهیم ساخت.
در ادامه، برخی جزئیات حذف خواهند شد زیرا ممکن است ما را از نکته اصلی، یعنی سازماندهی کد، منحرف کنند. خوانندگان علاقهمند میتوانند کد کامل را در وبسایت این سند بیابند.
8.6.2. پروژه STS
![]() |
- در [1]، کد جاوا؛
- در [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>istia.st.rdvmedecins</groupId>
<artifactId>rdvmedecins-springthymeleaf-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-springthymeleaf-server</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>istia.st.rdvmedecins</groupId>
<artifactId>rdvmedecins-webjson-client-console</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<properties>
<start-class>rdvmedecins.springthymeleaf.server.boot.Boot</start-class>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
...
</project>
- خطوط ۱۶–۱۹: پروژه یک پروژه Thymeleaf است؛
- خطوط ۲۰–۲۴: که بر لایه [DAO] که همین حالا ساختهایم متکی است؛
پیکربندی جاوا توسط دو فایل انجام میشود:
![]() |
لایه [web] توسط فایل زیر [WebConfig] پیکربندی میشود:
package rdvmedecins.springthymeleaf.server.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
@EnableAutoConfiguration
public class WebConfig extends WebMvcConfigurerAdapter {
// ----------------- QZXW2HTML لایه پیکربندی BW3dlYl0ZQX
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("i18n/messages");
return messageSource;
}
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setPrefix("classpath:/templates/");
templateResolver.setSuffix(".xml");
templateResolver.setTemplateMode("HTML5");
templateResolver.setCacheable(true);
templateResolver.setCharacterEncoding("UTF-8");
return templateResolver;
}
@Bean
SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
// پیکربندی DispatcherServlet برای هدرها CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
}
ما قبلاً با تمام اجزای این پیکربندی برخورد کردهایم. بیایید به سادگی به یاد بیاوریم که خطوط ۴۲–۴۷ زمانی ضروری هستند که بخواهیم با درخواستهای بیندامنهای (cross-domain) از سرور پرسوجو کنیم (CORS). این مورد در اینجا صادق است.
کلاس [AppConfig] کل برنامه را پیکربندی میکند:
package rdvmedecins.springthymeleaf.server.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.client.config.DaoConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {
// مدیر / مدیر
private final String USER_INIT = "admin";
private final String MDP_USER_INIT = "admin";
// ریشه سرویس وب / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// زمانانتظار به میلیثانیه
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
...
}
- خط ۱۱: [AppConfig] پیکربندی را از لایه [DAO] و لایه [web] وارد میکند؛
- خطوط ۱۵–۱۶: اعتبارنامههایی که به برنامه اجازه دسترسی به فرآیند راهاندازی برنامه را میدهند تا پزشکان و مشتریان را در حافظه پنهان ذخیره کند؛
- خط ۱۸: URL برای سرویس وب / jSON [Web1];
- خط ۲۰: timeout برای فراخوانیهای HTTP برنامه؛
- خط ۲۲: یک بول برای فعال یا غیرفعال کردن فراخوانیهای بیندامنهای؛
در نهایت، در [application.properties]، سرور Tomcat برای اجرا روی پورت 8081 پیکربندی شده است:
![]() |
server.port=8081
8.6.3. ویژگیهای برنامه
این موارد در بخش 8.2 توضیح داده شدند. اکنون آنها را خلاصه میکنیم. با استفاده از یک مرورگر وب، کاربر URL [http://localhost:8081/boot.html] را درخواست میکند:
![]() |
- به [1]، صفحه ورود برنامه؛
- [2] و [3]، نام کاربری و رمز عبور فردی است که مایل به استفاده از برنامه است. دو کاربر وجود دارد: admin/admin (نام کاربری/رمز عبور) با نقشی (ADMIN) و user/user با نقشی (USER). تنها نقش ADMIN مجاز به استفاده از برنامه است. نقش USER صرفاً برای نمایش پاسخ سرور در این مورد استفاده گنجانده شده است؛
- در [4]، دکمهای که برای ورود به سرور استفاده میشود؛
- در [5]، زبان برنامه. دو گزینه وجود دارد: فرانسوی (پیشفرض) و انگلیسی؛
- در [6]، URL از سرور [rdvmedecins-springthymeleaf-server]؛
![]() |
- در [1]، شما وارد میشوید؛
![]() |
- پس از ورود به سیستم، میتوانید پزشکی را که مایل به رزرو وقت ملاقات با او هستید ([2]) و تاریخ قرار ملاقات ([3]) را انتخاب کنید. به محض انتخاب پزشک و تاریخ، تقویم به طور خودکار نمایش داده میشود:
![]() |
- پس از دسترسی به تقویم پزشک، میتوانید یک نوبت رزرو کنید [5]؛
![]() |
- در [6]، بیمار را برای قرار ملاقات انتخاب کرده و این انتخاب را در [7] تأیید میکنید؛
![]() |
پس از تأیید قرار ملاقات، شما به طور خودکار به تقویم بازمیگردید که در آن قرار ملاقات جدید اکنون فهرست شده است. این قرار ملاقات را میتوان در تاریخ بعدی از طریق [8] حذف کرد.
ویژگیهای اصلی توضیح داده شدند. آنها ساده هستند. بیایید با مدیریت زبان به پایان برسیم:
![]() |
- در [1]، از فرانسوی به انگلیسی تغییر میدهید؛
![]() |
- در [2]، نما به انگلیسی، شامل تقویم، تغییر میکند؛
8.6.4. مرحله ۱: معرفی چارچوب Bootstrap CSS
![]() |
در کلاینت وب بالا، صفحات HTML از چارچوب Bootstrap CSS [http://getbootstrap.com/] استفاده خواهند کرد که اکنون آن را معرفی میکنیم.
8.6.4.1. پروژه نمونه
پروژهٔ نمونه به شرح زیر خواهد بود:
![]() |
- به [1]: پروژه به طور کلی؛
- در [2]: کد جاوا؛
- در [3]: اسکریپتهای جاوااسکریپت؛
![]() |
- در [4]: کتابخانههای جاوااسکریپت؛
- در [5]: ویوهای Thymeleaf؛
- در [6]: صفحات سبک؛
8.6.4.1.1. پیکربندی Maven
فایل [pom.xml] برای یک پروژه Thymeleaf Maven است:
<?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>istia.st</groupId>
<artifactId>rdvmedecins-webjson-client-bootstrap</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-client-bootstrap</name>
<description>Démos Bootstrap</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.0.RELEASE</version>
<relativePath /> <!-- جستجوی والد از مخزن -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.rdvmedecins.BootstrapDemo</start-class>
<java.version>1.7</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
8.6.4.1.2. پیکربندی جاوا
![]() |
کلاس [BootstrapDemo] برنامه Spring/Thymeleaf را پیکربندی میکند:
package istia.st.rdvmedecins;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
@EnableAutoConfiguration
@ComponentScan({ "istia.st.rdvmedecins" })
public class BootstrapDemo extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(BootstrapDemo.class, args);
}
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setPrefix("classpath:/templates/");
templateResolver.setSuffix(".xml");
templateResolver.setTemplateMode("HTML5");
templateResolver.setCacheable(true);
templateResolver.setCharacterEncoding("UTF-8");
return templateResolver;
}
}
ما قبلاً با این نوع کد مواجه شدهایم.
8.6.4.1.3. کنترلکننده Spring
![]() |
کنترلکننده [BootstrapController] به شرح زیر است:
package istia.st.rdvmedecins;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class BootstrapController {
@RequestMapping(value = "/bs-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bso1() {
return "bs-01";
}
@RequestMapping(value = "/bs-02", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs02() {
return "bs-02";
}
@RequestMapping(value = "/bs-03", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs03() {
return "bs-03";
}
@RequestMapping(value = "/bs-04", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs04() {
return "bs-04";
}
@RequestMapping(value = "/bs-05", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs05() {
return "bs-05";
}
@RequestMapping(value = "/bs-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs06() {
return "bs-06";
}
@RequestMapping(value = "/bs-07", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs07() {
return "bs-07";
}
@RequestMapping(value = "/bs-08", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs08() {
return "bs-08";
}
}
اقدامات تنها برای نمایش ویوهایی هستند که توسط Thymeleaf پردازش شدهاند.
8.6.4.1.4. فایل [application.properties]
فایل [application.properties] سرور Tomcat جاسازیشده را پیکربندی میکند:
server.port=8082
8.6.4.2. مثال ۱: جمبوتراون
اقدام [/bs-01] نمای زیر را نمایش میدهد [bs-01.xml]:
![]() |
نما [bs-01.xml] به شرح زیر است:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!--هسته Bootstrap CSS -->
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
</head>
<body id="body">
<div class="container">
<!-- بوتاسترپ جامبوترون -->
<div th:include="jumbotron"></div>
<!-- محتوا -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- خطا -->
<div id="erreur" class="alert alert-danger">
<span>Ici, un texte d'erreur</span>
</div>
</div>
</body>
</html>
- خط ۷: فایل CSS از فریمورک Bootstrap؛
- خط ۸: یک فایل محلی CSS؛
- خط ۱۳: نمایش [1];
- خطوط ۱۹–۲۱: نمایش [2];
- خط ۱۱: کلاس CSS [container] یک ناحیه نمایش را در داخل مرورگر تعریف میکند؛
- خط ۱۹: کلاس CSS [alert] یک ناحیهٔ رنگی را نمایش میدهد. کلاس [alert-danger] از یک رنگ از پیش تعریفشده استفاده میکند. چندین مورد از اینها وجود دارد: [alert-info, alert-warning,...];
جامبتورون [1] توسط نمای زیر [jumbotron.xml] تولید میشود:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<!-- بوتاسترپ جامبوترون -->
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="resources/images/caduceus.jpg" alt="RvMedecins" />
</div>
<div class="col-md-10">
<h1>
Les Médecins
<br />
associés
</h1>
</div>
</div>
</div>
</section>
- خط ۴: این ناحیه دارای کلاس CSS [jumbotron] است؛
- خط ۵: کلاس [row] یک ردیف با ۱۲ ستون را تعریف میکند؛
- خط ۶: کلاس [col-md-2] یک ناحیهٔ دو ستونی را در داخل ردیف تعریف میکند؛
- خط ۷: یک تصویر در این دو ستون قرار میگیرد؛
- خطوط ۹–۱۵: متن در ۱۰ ستون باقیمانده قرار میگیرد؛
8.6.4.3. مثال ۲: نوار ناوبری
عمل [/bs-02] نمای زیر را نمایش میدهد [bs-02.xml]:
![]() |
ویژگی جدید نوار ناوبری [1] با فرم ورودی و دکمههای آن است:
نمای [bs-02.xml] به شرح زیر است:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- هستهٔ بوتاسترپ CSS -->
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- اسکریپتها JS -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/js/bs-02.js"></script>
</head>
<body id="body">
<div class="container">
<!--نوار ناوبری -->
<div th:include="navbar1"></div>
<!-- بوتاسترپ جامبوترون -->
<div th:include="jumbotron"></div>
<!--محتوا -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- اطلاعات -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- خط ۱۰: jQuery وارد شده است؛
- خط ۱۱: یک اسکریپت محلی JS;
- خط ۱۶: نوار ناوبری؛
نوار ناوبری توسط نمای زیر، [navbar1.xml]، تولید میشود:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- فرم شناسایی -->
<div class="navbar-form navbar-right" role="form" id="formulaire" method="post">
<div class="form-group">
<input type="text" placeholder="Utilisateur" class="form-control" />
</div>
<div class="form-group">
<input type="password" placeholder="Mot de passe" class="form-control" />
</div>
<button type="button" class="btn btn-success" onclick="javascript:connecter()">Connexion</button>
</div>
</div>
</div>
</div>
</section>
![]() |
- خط ۳: کلاس [navbar] نوار ناوبری را استایل میدهد. کلاس [navbar-inverse] به آن پسزمینهای سیاه میدهد. کلاس [navbar-fixed-top] تضمین میکند که وقتی صفحهای که توسط مرورگر نمایش داده میشود پیمایش میشود، نوار ناوبری در بالای صفحه باقی بماند؛
- خطوط ۵–۱۳: ناحیه [1] را تعریف میکنند. این معمولاً مجموعهای از کلاسهاست که من آنها را درک نمیکنم. من این کامپوننت را همانطور که هست استفاده میکنم؛
- خطوط 14–26: ناحیه «واکنشگرا»ی نوار فرمان را تعریف میکنند. در یک گوشی هوشمند، این ناحیه به یک منو تبدیل میشود؛
- خط ۱۵: تصویری که در حال حاضر پنهان است؛
- خطوط 17–25: کلاس [navbar-form] یک فرم را در نوار فرمان سبکدهی میکند. کلاس [navbar-right] آن را در سمت راست فرم قرار میدهد؛
- خطوط ۲۱–۲۳: دو فیلد ورودی فرم در خط ۱۷، [2]. آنها در یک کلاس [form-group] قرار دارند که عناصر یک فرم را استایل میکند، و هر یک از آنها دارای کلاس [form-control] است؛
- خط ۲۴: کلاس [btn] که یک دکمه را تعریف میکند، که توسط کلاس [btn-success] تقویت شده است، کلاسی که رنگ سبز را به آن میدهد؛
- خط ۲۴: وقتی دکمه [Connexion] کلیک میشود، تابع زیر JS اجرا میشود:
function connecter() {
showInfo("Connexion demandée...");
}
function showInfo(message) {
$("#info").text(message);
}
در اینجا یک مثال آورده شده است:

8.6.4.4. مثال ۳: دکمه فهرست
عمل [/bs-03] نمای زیر را نمایش میدهد [bs-03.xml]:
![]() |
- ویژگی جدید، لیستبکس [1] است که به «لیست کشویی» نیز معروف است؛
کد مربوط به نمای [bs-03.xml] به شرح زیر است:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!--هسته بوتاسترپ CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!--هسته Bootstrap JavaScript ================================================== -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script src="resources/vendor/bootstrap.js"></script>
<!-- اسکریپت محلی -->
<script type="text/javascript" src="resources/js/bs-03.js"></script>
</head>
<body id="body">
<div class="container">
<!--نوار ناوبری -->
<div th:include="navbar2"></div>
<!-- بوتاسترپ جمبوترون -->
<div th:include="jumbotron"></div>
<!-- محتوا -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- اطلاعات -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- خط ۱۱: دکمهٔ کشویی به فایل Bootstrap JS نیاز دارد؛
- خط ۱۸: نوار ناوبری جدید؛
نما [navbar2.xml] به شرح زیر است:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- فرم ورود -->
<div class="navbar-form navbar-right" role="form" id="formulaire" method="post">
<div class="form-group">
<input type="text" placeholder="Utilisateur" class="form-control" />
</div>
<div class="form-group">
<input type="password" placeholder="Mot de passe" class="form-control" />
</div>
<button type="button" class="btn btn-success" onclick="javascript:connecter()">Connexion</button>
<!-- زبانها -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langues</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')">Français</a>
</li>
<li>
<a href="javascript:setLang('en')">English</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!--صفحه اصلی -->
<script th:inline="javascript">
/*<![CDATA[*/
// ابتداییسازی صفحه
initNavBar2();
/*]]>*/
</script>
</section>
- خطوط ۲۵–۴۰: دکمه لیست را تعریف کنید؛
- خط ۲۷: کلاس [btn-danger] رنگ قرمز را به آن میدهد؛
- خطوط ۳۲–۳۹: آیتمهای لیست. اینها لینکهایی هستند که هر کدام با یک تابع JS مرتبط هستند؛
- خطوط ۴۶–۵۱: یک اسکریپت JS که پس از بارگذاری سند اجرا میشود؛
اسکریپت JS [bs-03.js] به شرح زیر است:
function initNavBar2() {
// لیست کشویی زبان
$('.dropdown-toggle').dropdown();
}
function connecter() {
showInfo("Connexion demandée...");
}
function setLang(lang) {
var msg;
switch (lang) {
case 'fr':
msg = "Vous avez choisi la langue française...";
break;
case 'en':
msg = "You have selected english language...";
break;
}
showInfo(msg);
}
function showInfo(message) {
$("#info").text(message);
}
- خطوط ۱–۴: تابع init[dropdown] که [dropdown] را مقداردهی اولیه میکند. [$('.dropdown-toggle')] عنصر با کلاس [dropdown-toggle] را پیدا میکند. این دکمه لیست (خط ۲۸ از نما) است. تابع JS [dropdown()]، که در فایل JS [bootstrap.js] تعریف شده است، روی آن اعمال میشود. تنها پس از این عملیات است که دکمه مانند یک دکمه فهرست عمل میکند؛
- خطوط ۱۰–۲۱: تابع هنگام انتخاب یک زبان اجرا میشود؛
در اینجا یک مثال آورده شده است:

8.6.4.5. مثال ۴: یک منو
عمل [/bs-04] نمای زیر [bs-04.xml] را نمایش میدهد:
![]() |
یک منو ([1]) اضافه شده است.
نما [bs-04.xml] به شرح زیر است:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!--هسته بوتاسترپ CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!--هسته Bootstrap JavaScript ================================================== -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script src="resources/vendor/bootstrap.js"></script>
<!-- اسکریپت محلی -->
<script type="text/javascript" src="resources/js/bs-04.js"></script>
</head>
<body id="body">
<div class="container">
<!--نوار ناوبری -->
<div th:include="navbar3"></div>
<!-- بوتاسترپ جمبوترون -->
<div th:include="jumbotron"></div>
<!-- محتوا -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- اطلاعات -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- خط ۱۸: یک نوار ناوبری جدید درج میشود؛
نما [navbar3.xml] به شرح زیر است:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="collapse navbar-collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<ul class="nav navbar-nav">
<li class="active" id="lnkAfficherAgenda">
<a href="javascript:afficherAgenda()">Agenda </a>
</li>
<li class="active" id="lnkAccueil">
<a href="javascript:retourAccueil()">Retour Accueil </a>
</li>
<li class="active" id="lnkRetourAgenda">
<a href="javascript:retourAgenda()">Retour Agenda </a>
</li>
<li class="active" id="lnkValiderRv">
<a href="javascript:validerRv()">Valider </a>
</li>
</ul>
<!-- دکمههای سمت راست -->
<div class="navbar-form navbar-right" role="form">
<!-- خروج -->
<button type="button" class="btn btn-success" onclick="javascript:deconnecter()">Déconnexion</button>
<!-- زبانها -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langues</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')">Français</a>
</li>
<li>
<a href="javascript:setLang('en')">English</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- صفحه اصلی -->
<script th:inline="javascript">
/*<![CDATA[*/
// در حال راهاندازی صفحه
initNavBar3();
/*]]>*/
</script>
</section>
- خطوط ۱۶–۲۹: ایجاد منو با چهار گزینه، که هر کدام به اسکریپت JS متصل هستند؛
- خطوط ۵۵–۶۰: اسکریپتی که هنگام بارگذاری صفحه اجرا میشود؛
اسکریپت JS [bs-04.js] به شرح زیر است:
...
function initNavBar3() {
// لیست کشویی زبان
$('.dropdown-toggle').dropdown();
// تصویر متحرک
loading = $("#loading");
loading.hide();
}
function afficherAgenda() {
showInfo("option [Agenda] cliquée...");
}
function retourAccueil() {
showInfo("option [Retour accueil] cliquée...");
}
function retourAgenda() {
showInfo("option [Retour agenda] cliquée...");
}
function validerRv() {
showInfo("option [Valider] cliquée...");
}
function setMenu(show) {
// لینکهای منو
var lnkAfficherAgenda = $("#lnkAfficherAgenda");
var lnkAccueil = $("#lnkAccueil");
var lnkValiderRv = $("#lnkValiderRv");
var lnkRetourAgenda = $("#lnkRetourAgenda");
// ما آنها را در یک فرهنگ لغت قرار میدهیم
var options = {
"lnkAccueil" : lnkAccueil,
"lnkAfficherAgenda" : lnkAfficherAgenda,
"lnkValiderRv" : lnkValiderRv,
"lnkRetourAgenda" : lnkRetourAgenda
}
// تمام پیوندها پنهان هستند
for ( var key in options) {
options[key].hide();
}
// ما آنهایی را که درخواست شدهاند نمایش میدهیم
for (var i = 0; i < show.length; i++) {
var option = show[i];
options[option].show();
}
}
- خطوط ۲–۱۸: تابع инициализация صفحه؛
- خط ۴: برای نمایش دکمه انتخاب زبان؛
- خطوط ۶–۷: تصویر متحرک پنهان است؛
- خطوط 26–48: تابع [setMenu] که به شما امکان میدهد مشخص کنید کدام گزینهها باید قابل مشاهده باشند؛
بیایید به کنسول توسعهدهنده (Ctrl-Shift-I) برویم و کد زیر را وارد کنیم: [1]:
![]() |
سپس به مرورگر بازگردید. منو تغییر کرده است [2]:
8.6.4.6. مثال ۵: یک لیست کشویی
عمل [/bs-05] نمای زیر را نمایش میدهد [bs-05.xml]:
![]() |
ویژگی جدید در [1] قرار دارد. در اینجا ما از یک کامپوننت ارائهشده خارج از Bootstrap، [bootstrap-select] [http://silviomoreto.github.io/bootstrap-select/] استفاده میکنیم.
کد نمای [bs-05.xml] به شرح زیر است:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!--هسته Bootstrap CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!--هسته Bootstrap JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<!-- اسکریپت محلی -->
<script type="text/javascript" src="resources/js/bs-05.js"></script>
</head>
<body id="body">
<div class="container">
<!--نوار ناوبری -->
<div th:include="navbar3"></div>
<!-- بوتاسترپ جامبوترون -->
<div th:include="jumbotron"></div>
<!-- محتوا -->
<div id="content" th:include="choixmedecin">
</div>
<!-- اطلاعات -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- خط ۸: فایل CSS مورد نیاز برای لیست کشویی؛
- خط ۱۳: فایل JS مورد نیاز برای لیست کشویی؛
- خط ۲۴: لیست کشویی؛
نما [choixmedecin.xml] به شرح زیر است:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info">Veuillez choisir un médecin</div>
<div class="row">
<div class="col-md-3">
<h2>Médecin</h2>
<select id="idMedecin" class="combobox" data-style="btn-primary">
<option value="1">Mme Marie Pélissier</option>
<option value="2">Mr Jean Pardon</option>
<option value="3">Mlle Jeanne Jirou</option>
<option value="4">Mr Paul Macou</option>
</select>
</div>
</div>
<!-- اسکریپت محلی -->
<script th:inline="javascript">
/*<![CDATA[*/
// ابتداییسازی صفحه
initChoixMedecin();
/*]]>*/
</script>
</section>
- خطوط ۷–۱۲: این یک تگ استاندارد [select] است، اما با یک کلاس خاص، [combobox]. ویژگی [data-style="btn-primary"] رنگ آبی را به کامپوننت میدهد؛
- خطوط ۱۶–۲۱: اسکریپتی که هنگام بارگذاری صفحه اجرا میشود؛
فایل JS [bs-05.js] به شرح زیر است:
...
function afficherAgenda() {
var idMedecin = $('#idMedecin option:selected').val();
showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin);
}
function initChoixMedecin() {
// منوی کشویی پزشکان
$('#idMedecin').selectpicker();
// منو
setMenu([ "lnkAfficherAgenda" ]);
}
- خطوط ۷–۱۲: تابع اجرا شده هنگام بارگذاری صفحه؛
- خط ۹: دستوری که [select] را در صفحه به یک لیست کشویی Bootstrap تبدیل میکند. [$('#idMedecin')] به [select] ارجاع میدهد. (خط ۷ از نمای [choixmedecin]) و تابع JS [selectpicker] از فایل JS [bootstrap-select.js] میآید؛
- خط ۱۱: تنها یکی از گزینههای منو نمایش داده میشود؛
- خطوط ۲–۵: تابع JS هنگامی که گزینه منوی [Agenda] کلیک میشود، اجرا میگردد؛
- خط ۳: مقدار گزینهٔ انتخابشده در لیست کشویی بازیابی میشود: [$('#idMedecin option:selected')] ابتدا کامپوننت [id=idMedecin] را پیدا میکند و سپس در داخل آن، گزینهٔ انتخابشده را. سپس عملیات [..].val() مقدار عنصر یافتشده را بازیابی میکند، یعنی ویژگی [value] از گزینهٔ انتخابشده؛
در اینجا مثالی از انتخاب یک پزشک آورده شده است:
![]() |
8.6.4.7. مثال ۶: یک تقویم
عمل [/bs-06] نمای زیر [bs-06.xml] را نمایش میدهد:

انتخاب یک پزشک یا یک تاریخ، تابع JS را فعال میکند که هم پزشک انتخابشده و هم تاریخ انتخابشده را نمایش میدهد. در اینجا یک مثال آمده است:
![]() |
با استفاده از دکمه «فهرست زبانها»، میتوانید تقویم (و فقط تقویم) را به زبان انگلیسی تغییر دهید:

این پیچیدهترین مثال در این مجموعه است. تقویم یک کامپوننت [bootstrap-datepicker] [http://eternicode.github.io/bootstrap-datepicker] است.
نما [bs-06.xml] به شرح زیر است:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- هستهٔ بوتاسترپ CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!--هسته بوتاسترپ JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
<!-- اسکریپت محلی -->
<script type="text/javascript" src="resources/js/bs-06.js"></script>
</head>
<body id="body">
<div class="container">
<!--نوار ناوبری -->
<div th:include="navbar3"></div>
<!-- بوتاسترپ جمبوترون -->
<div th:include="jumbotron"></div>
<!-- محتوا -->
<div id="content" th:include="choixmedecinjour">
</div>
<!-- اطلاعات -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- خط ۸: فایل CSS از کامپوننت [bootstrap-datepicker];
- خط ۱۶: فایل JS از کامپوننت [bootstrap-datepicker];
- خط ۱۷: فایل JS برای مدیریت یک تقویم فرانسوی. به طور پیشفرض، به زبان انگلیسی است؛
- خط ۱۵: فایل JS از کتابخانهای به نام [moment]، که دسترسی به طیف گستردهای از توابع محاسبه زمان [http://momentjs.com/] را فراهم میکند؛
- خط ۲۸: نمای تقویم؛
نما [choixmedecinjour.xml] به شرح زیر است:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info">Veuillez choisir un médecin et une date</div>
<div class="row">
<div class="col-md-3">
<h2>Médecin</h2>
<select id="idMedecin" class="combobox" data-style="btn-primary">
<option value="1">Mme Marie Pélissier</option>
<option value="2">Mr Jean Pardon</option>
<option value="3">Mlle Jeanne Jirou</option>
<option value="4">Mr Paul Macou</option>
</select>
</div>
<div class="col-md-3">
<h2>Date</h2>
<section id="calendar_container">
<div id="calendar" class="input-group date">
<input id="displayjour" type="text" class="form-control btn-primary" disabled="true">
<span class="input-group-addon">
<i class="glyphicon glyphicon-th"></i>
</span>
</input>
</div>
</section>
</div>
</div>
<!-- اسکریپت محلی -->
<script th:inline="javascript">
/*<![CDATA[*/
// ابتداییسازی صفحه
initChoixMedecinJour();
/*]]>*/
</script>
</section>
- خطوط ۱۷–۲۳: تقویم؛
- خط ۱۸: کلاس [btn-primary] رنگ آبی را به آن میدهد؛
- خط ۱۸: ویژگی [disabled="true"] به این معنی است که تاریخ را نمیتوان به صورت دستی وارد کرد. باید از تقویم استفاده کنید؛
- خط ۱۶: تقویم در یک بخش [id="calendar_container"] قرار داده شده است. برای تغییر زبان تقویم، باید آن را حذف کرده و سپس مجدداً تولید کنید. بنابراین باید محتویات کامپوننت [id="calendar_container"] را حذف کرده و سپس تقویم جدید را با زبان جدید وارد کنید؛
- خطوط ۲۸–۳۳: کد инициализация صفحه؛
فایل JS [bs-06.js] به شرح زیر است:
...
var calendar_infos = {};
function initChoixMedecinJour() {
// تقویم
var calendar_container = $("#calendar_container");
calendar_infos = {
"container" : calendar_container,
"html" : calendar_container.html(),
"today" : moment().format('YYYY-MM-DD'),
"langue" : "fr"
}
// ایجاد تقویم
updateCalendar();
//انتخاب پزشک
$('#idMedecin').selectpicker();
$('#idMedecin').change(function(e) {
afficherAgenda();
})
// منو
setMenu([]);
}
- خط ۲: تقویم توسط چندین تابع، JS، مدیریت میشود. متغیر [calendar_infos] اطلاعات مربوط به تقویم را جمعآوری میکند. این یک متغیر سراسری است تا بتوان از طریق توابع مختلف به آن دسترسی داشت؛
- خط ۶: کانتینر تقویم شناسایی میشود؛
- خطوط ۷–۱۲: اطلاعات ذخیره شده برای تقویم؛
- خط ۸: مرجعی به کانتینر آن؛
- خط ۹: کد تقویم HTML. با این دو قطعه اطلاعات، میتوانیم تقویم را حذف کرده و دوباره آن را تولید کنیم،
- خط ۱۰: تاریخ امروز به فرمت [aaaa-mm-jj]،
- خط ۱۱: زبان تقویم؛
- خط ۱۴: ایجاد تقویم؛
- خط ۱۶: لیست کشویی پزشکان؛
- خطوط 17–19: هر زمان که مقدار انتخابشده در این لیست کشویی تغییر کند، متد [afficherAgenda] اجرا خواهد شد؛
- خط 21: بدون منو در نوار ناوبری؛
تابع [updateCalendar] به شرح زیر است:
function updateCalendar(renew) {
if (renew) {
// تازه کردن تقویم فعلی
calendar_infos.container.html(calendar_infos.html);
}
// ابتداییسازی تقویم
var calendar = $("#calendar");
var settings = {
format : "yyyy-mm-dd",
startDate : calendar_infos.today,
language : calendar_infos.langue,
};
calendar.datepicker(settings);
// انتخاب تاریخ فعلی
if (calendar_infos.date) {
calendar.datepicker('setDate', calendar_infos.date)
}
// رویدادها
calendar.datepicker().on('hide', function(e) {
// نمایش روز انتخابشده
displayJour();
});
calendar.datepicker().on('changeDate', function(e) {
// ثبت تاریخ جدید
calendar_infos.date = moment(calendar.datepicker('getDate')).format("YYYY-MM-DD");
//نمایش جزئیات تقویم
afficherAgenda();
//نمایش روز انتخابشده
displayJour();
});
// نمایش روز انتخابشده
displayJour();
}
- خط ۱: تابع [updateCalendar] پارامتری را میپذیرد که ممکن است وجود داشته باشد یا نداشته باشد. اگر این پارامتر وجود داشته باشد، تقویم با استفاده از اطلاعات موجود در [calendar_infos] مجدداً تولید میشود (خط ۴)؛
- خط ۷: به تقویم ارجاع داده شده است؛
- خطوط ۸–۱۲: پارامترهای инициализатсия آن؛
- خط ۹: فرمت تاریخ که توسط [aaaa-mm-jj] مدیریت میشود،
- خط ۱۰: اولین تاریخی که میتوان در تقویم انتخاب کرد. در این مورد، تاریخ امروز. تاریخهای قبل از این قابل انتخاب نیستند،
- خط ۱۱: زبان تقویم. دو زبان وجود خواهد داشت: ['en'] و ['fr'];
- خط ۱۳: تقویم پیکربندی شده است؛
- خطوط ۱۵–۱۷: اگر تاریخ [calendar_infos] تنظیم شده باشد، آنگاه این تاریخ به عنوان تاریخ فعلی در تقویم استفاده میشود؛
- خطوط ۱۹–۲۲: هر بار که تقویم بسته میشود، تاریخ انتخابشده نمایش داده خواهد شد؛
- خطوط ۲۳–۳۰: هرگاه تاریخ در تقویم تغییر کند:
- خط ۲۵: تاریخ انتخابشده در [calendar_infos] ثبت میشود،
- خط ۲۷: اطلاعات مربوط به دفترچه یادداشت نمایش داده میشود،
- خط ۲۹: روز انتخابشده نمایش داده میشود؛
- خط ۳۲: روز انتخابشده نمایش داده میشود، در صورتی که وجود داشته باشد؛
روش [displayJour] که روز انتخابی را نمایش میدهد، به شرح زیر است:
//روز انتخابشده را نمایش میدهد
function displayJour() {
if (calendar_infos.date) {
var displayjour = $("#displayjour");
moment.locale(calendar_infos.langue);
jour = moment(calendar_infos.date).format('LL');
displayjour.val(jour);
}
}
- خط ۳: اگر تاریخ قبلاً انتخاب شده باشد (در ابتدا، هیچ تاریخی در تقویم انتخاب نشده است)؛
- خط ۴: کامپوننتی را که تاریخ باید در آن نوشته شود، پیدا کنید؛
- خط ۵: این تاریخ میتواند به زبان انگلیسی یا فرانسوی نوشته شود. ما زبان کتابخانه [moment] را تنظیم میکنیم؛
- خط ۶: تاریخ انتخابشده به زبان انتخابشده و به صورت فرمت طولانی نمایش داده میشود؛
- خط ۷: این تاریخ نمایش داده میشود؛
در اینجا دو مثال آورده شده است:
![]() | ![]() |
هنگامی که یک دکتر یا تاریخ تغییر میکند، متد [afficherAgenda] اجرا میشود:
function afficherAgenda() {
// پزشک و تاریخ را نمایش میدهد
var idMedecin = $('#idMedecin option:selected').val();
if (calendar_infos.date) {
showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin + " et le jour " + calendar_infos.date);
}
}
8.6.4.8. مثال ۷: یک جدول «واکنشگرا» HTML
توجه: «responsive» یک اصطلاح انگلیسی است که نشان میدهد یک مؤلفه قادر است خود را با اندازه صفحهای که روی آن نمایش داده میشود، تطبیق دهد. ما یک مثال از این را نشان خواهیم داد.
عمل [/bs-07] نمای زیر [bs-07.xml] (تمامصفحه) را نمایش میدهد:
![]() |
ویژگی جدید، جدول HTML [1] است. این جدول توسط کتابخانه JS [footable]: [https://github.com/fooplugins/FooTable] مدیریت میشود.
اگر اندازه پنجره مرورگر را تغییر دهید، نتیجه زیر را مشاهده میکنید:
![]() |
- جدول HTML خود را با اندازه صفحه نمایش تطبیق داده است؛
- در [1]، برای مشاهده پیوند [Réserver]، باید روی نماد [+] کلیک کنید؛
- در [2]، این چیزی است که هنگام کلیک بر روی نماد [+] میبینید؛
نمای [bs-07.xml] به شرح زیر است:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- هستهٔ Bootstrap CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
<link rel="stylesheet" type="text/css" href="resources/css/footable.core.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!--هسته Bootstrap JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
<script type="text/javascript" src="resources/vendor/footable.js"></script>
<!-- اسکریپت محلی -->
<script type="text/javascript" src="resources/js/bs-07.js"></script>
</head>
<body id="body">
<div class="container">
<!--نوار ناوبری -->
<div th:include="navbar3" />
<!-- بوتاسترپ جمبوترون -->
<div th:include="jumbotron" />
<!-- محتوا -->
<div id="content" th:include="choixmedecinjour" />
<div id="agenda" th:include="agenda" />
<!-- اطلاعات -->
<div class="alert alert-success">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- خط ۱۰: CSS از کتابخانه [footable];
- خط ۱۹: JS از کتابخانه [footable];
- خط ۳۱: جدول HTML از یک دفترچه یادداشت؛
نمای [agenda.xml] به شرح زیر است:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="row alert alert-danger">
<div class="col-md-6">
<table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span>Créneau horaire</span>
</th>
<th>
<span>Client</span>
</th>
<th data-hide="phone">
<span>Action</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span class='status-metro status-active'>
9h00-9h20
</span>
</td>
<td>
<span></span>
</td>
<td>
<a href="javascript:reserver(14)" class="status-metro status-active">
Réserver
</a>
</td>
</tr>
<tr>
<td>
<span class='status-metro status-suspended'>
9h20-9h40
</span>
</td>
<td>
<span>Mme Paule MARTIN</span>
</td>
<td>
<a href="javascript:supprimer(17)" class="status-metro status-suspended">
Supprimer
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- صفحه اصلی -->
<script th:inline="javascript">
/*<![CDATA[*/
//صفحهٔ اولیهٔ صفحه راهاندازی شد
initAgenda();
/*]]>*/
</script>
</body>
</html>
- خط ۴: جدول را در یک ردیف [row] و یک کادر رنگی [alert alert-danger] درج میکند؛
- خط ۵: جدول ۶ ستون را در بر خواهد گرفت [col-md-6];
- خط ۶: جدول HTML با استفاده از بوتاسترپ [class='table'] استایلدهی میشود؛
- خط ۹: ویژگی [data-toggle] ستونی را مشخص میکند که حاوی نماد [+/-] است، که ردیف را باز یا جمع میکند؛
- خط ۱۵: ویژگی [data-hide='phone'] مشخص میکند که ستون در صورتی که اندازه صفحه نمایش برابر با اندازه صفحه نمایش تلفن همراه باشد، باید پنهان شود. مقدار «tablet» نیز میتواند استفاده شود؛
- خط ۳۱: تابع JS با لینک [Réserver] مرتبط است؛
- خط ۴۶: تابع JS با لینک [Supprimer] مرتبط است؛
- خطوط ۵۶–۶۱: inicialization صفحه؛
تعدادی از کلاسهای CSS که در بالا استفاده شدهاند، از فایل CSS [bootstrapDemo.css] گرفته شدهاند:
@CHARSET "UTF-8";
#th slots {
text-align: center;
}
#td حاشیهها {
text-align: center;
font-weight: bold;
}
.status-metro {
display: inline-block;
padding: 2px 5px;
color:#fff;
}
.status-metro.status-active {
background: #43c83c;
}
.status-metro.status-suspended {
background: #fa3031;
}
استایلهای [status-*] از مثالی در مورد نحوه استفاده از جدول [footable] که در وبسایت کتابخانه یافت میشود، گرفته شدهاند.
در فایل JS [bs-07.js]، صفحه به شرح زیر inicialize میشود:
function initAgenda() {
// برنامه زمانبندی
$("#creneaux").footable();
}
این همه است. [$("#creneaux")] به جدول HTML ارجاع میدهد که میخواهیم آن را «واکنشگرا» کنیم. علاوه بر این، توابع JS به دو لینک [Réserver] و [Supprimer] متصل هستند:
function reserver(idCreneau) {
showInfo("Réservation du créneau n° " + idCreneau);
}
function supprimer(idRv) {
showInfo("Suppression du rv n° " + idRv);
}
8.6.4.9. مثال ۸: یک کادر مدال
عمل [/bs-08] نمای زیر را نمایش میدهد [bs-08.xml]:

در حالی که قبلاً، کلیک بر روی لینک [Réserver] اطلاعات را در کادر اطلاعات نمایش میداد، در اینجا ما یک کادر مدال برای انتخاب مشتری برای RV نمایش خواهیم داد:

کامپوننت مورد استفاده [bootstrap-modal] [https://github.com/jschr/bootstrap-modal/] است.
نما [bs-08.xml] به شرح زیر است:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!--هسته Bootstrap CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
<link rel="stylesheet" type="text/css" href="resources/css/footable.core.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!--هستهٔ Bootstrap JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-modal.js"></script>
<script type="text/javascript" src="resources/vendor/footable.js"></script>
<!-- اسکریپت محلی -->
<script type="text/javascript" src="resources/js/bs-08.js"></script>
</head>
<body id="body">
<div class="container">
<!--نوار ناوبری -->
<div th:include="navbar3" />
<!-- بوتاسترپ جمبوترون -->
<div th:include="jumbotron" />
<!-- محتوا -->
<div id="content" th:include="choixmedecinjour" />
<div id="agenda" th:include="agenda-modal" />
<div th:include="resa" />
<!-- اطلاعات -->
<div class="alert alert-success">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- خط ۱۹: فایل JS مورد نیاز برای کادرهای مدال؛
- خط ۳۲: نمای [agenda-modal] از نمای [agenda] جز در یک جزئیات یکسان است: تابع JS که لینک [Réserver] را مدیریت میکند:
<a href="javascript:showDialogResa(14)" class="status-metro status-active">Réserver</a>
توابع [showDialogResa] مسئول نمایش کادر مدال برای انتخاب مشتری است؛
- خط ۳۳: ویوی [resa.xml]، پنجرهٔ مدال برای انتخاب مشتری است:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div id="resa" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">
</span>
</button>
<!-- <h4 class="modal-title">عنوان مودال -->
</div>
<div class="modal-body">
<div class="alert alert-info">
<h3>
<span>Prise de rendez-vous</span>
</h3>
</div>
<div class="row">
<div class="col-md-3">
<h2>Clients</h2>
<select id="idClient" class="combobox" data-style="btn-primary">
<option value="1">Mme Marguerite Planton</option>
<option value="2">Mr Maxime Franck</option>
<option value="3">Mlle Elisabeth Oron</option>
<option value="4">Mr Gaëtan Calot</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" onclick="javascript:cancelDialogResa()">Annuler</button>
<button type="button" class="btn btn-primary" onclick="javascript:validateResa()">Valider</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- صفحهٔ اولیه -->
<script th:inline="javascript">
/*<![CDATA[*/
// صفحه راهاندازی میشود
initResa();
/*]]>*/
</script>
</section>
- خطوط ۳–۳۷: پنجرهٔ مدال؛
- خطوط ۱۳–۳۰: محتوای این کادر (آنچه نمایش داده میشود)؛
- خطوط ۳۱–۳۴: دکمههای موجود در کادر گفتگو؛
- خط ۳۲: یک دکمه [Annuler] که توسط تابع JS [cancelDialogResa] مدیریت میشود؛
- خط ۳۳: یک دکمه [Valider] که توسط توابع JS و [validateResa] مدیریت میشود؛
- خطوط ۳۹–۴۴: اسکریپت инициализация کادر مدال؛
این کار نمای زیر را ایجاد میکند:
![]() |
توجه داشته باشید که کادر مدال به طور پیشفرض نمایش داده نمیشود. به همین دلیل است که هنگام شروع برنامه قابل مشاهده نیست، هرچند کد آن HTML در سند موجود است.
فایل JS [bs-08.js] به شرح زیر است:
var idCreneau;
var idClient;
var resa;
function showDialogResa(idCreneau) {
//شناسه اسلات ذخیره میشود
this.idCreneau = idCreneau;
// نمایش پنجرهٔ رزرو
var resa = $("#resa");
resa.modal('show');
// لاگ
showInfo("Réservation du créneau n° " + idCreneau);
}
function cancelDialogResa() {
// پنهان کردن کادر گفتگو
resa.modal('hide');
}
//تأیید رزرو
function validateResa() {
//بازیابی اطلاعات
var idClient = $('#idClient option:selected').val();
// پنهان کردن کادر گفتگو
resa.modal('hide');
// اطلاعات
showInfo("Réservation du créneau n° " + idCreneau + " pour le client n° " + idClient)
}
function initResa() {
// منوی کشویی مشتری
$('#idClient').selectpicker();
// جعبهی مدال
resa = $("#resa");
resa.modal({});
}
- خطوط ۳۰–۳۶: تابع برای راهاندازی کادر موال؛
- خط ۳۲: کادر مدال شامل یک لیست کشویی است که باید مقداردهی اولیه شود؛
- خطوط ۳۴–۳۵: инициалиزهسازی خودِ پنجرهٔ مدال؛
- خطوط ۵–۱۳: تابع JS متصل به لینک [Réserver];
- خط ۷: پارامتر تابع در متغیر سراسری تعریفشده در خط ۱ ذخیره میشود؛
- خطوط ۹–۱۰: کادر موдал نمایش داده میشود؛
- خط ۱۲: اطلاعات در کادر اطلاعات ثبت میشود؛
- خطوط ۱۵–۱۸: رسیدگی به دکمه [Annuler]. ما به سادگی پنجره مدال را مخفی میکنیم (خط ۱۷);
- خطوط ۲۱–۳۱: تابع JS به دکمه [Valider] متصل میشود؛
- خط ۲۳: ویژگی [value] را برای مشتری انتخابشده بازیابی کنید؛
- خط ۲۵: کادر گفتگو پنهان است؛
- خط ۲۷: دو مورد اطلاعات ثبت میشوند: شماره اسلات رزروشده و مشتری مربوطه؛
8.6.5. مرحله ۲: نوشتن ویوها
اکنون نماهای بازگرداندهشده توسط سرور [Web1] را همراه با قالبهایشان شرح میدهیم.
![]() |
8.6.5.1. ویوی [navbar-start]
این نوار ناوبری را در صفحهٔ بوت نمایش میدهد:

کد مربوط به [navbar-start.xml] به شرح زیر است:
<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- فرم ورود -->
<div class="navbar-form navbar-right" role="form" id="formulaire">
<div class="form-group">
<input type="text" th:placeholder="#{service.url}" class="form-control" id="urlService" />
</div>
<div class="form-group">
<input type="text" th:placeholder="#{username}" class="form-control" id="login" />
</div>
<div class="form-group">
<input type="password" th:placeholder="#{password}" class="form-control" id="passwd" />
</div>
<button type="button" class="btn btn-success" th:text="#{login}" onclick="javascript:connecter()">Sign in</button>
<!-- زبانها -->
<div class="btn-group">
<button type="button" class="btn btn-danger" th:text="#{langues}">Action</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')" th:text="#{langues.fr}" />
</li>
<li>
<a href="javascript:setLang('en')" th:text="#{langues.en}" />
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!--صفحه اصلی -->
<script th:inline="javascript">
/*<![CDATA[*/
// ابتداییسازی صفحه
initNavBarStart();
/*]]>*/
</script>
</section>
این نما قالب ندارد. دارای رویدادپردازهای زیر است:
رویداد | پردازشگر |
روی دکمه ورود کلیک کنید | |
روی لینک [Français] کلیک کنید | |
روی لینک [English] کلیک کنید |
8.6.5.2. نما [jumbotron]
این نما در زیر نوار ناوبری [navbar-start] در صفحهٔ بوت نمایش داده میشود:

کد آن [jumbotron.xml] به شرح زیر است:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<!--بوتاسترپ جمبوترون -->
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="resources/images/caduceus.jpg" alt="RvMedecins" />
</div>
<div class="col-md-10">
<h1 th:utext="#{application.header}" />
</div>
</div>
</div>
</section>
نما [jumbotron] نه قالب دارد و نه هیچ رویدادی.
8.6.5.3. نما [login]
این نمایی است که در زیر جمبوترون در صفحهٔ بوت نمایش داده میشود:

کد آن، [login.xml]، به شرح زیر است:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info" th:text="#{identification}">Identification
</div>
</section>
این نما نه قالب دارد و نه هیچ رویدادی.
8.6.5.4. نما [navbar-run]
این نوار ناوبری هنگام موفقیت اتصال نمایش داده میشود:

کد آن، [navbar-run.xml]، به شرح زیر است:
<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="collapse navbar-collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- دکمههای سمت راست -->
<form class="navbar-form navbar-right" role="form">
<!-- خروج -->
<button type="button" class="btn btn-success" th:text="#{options.deconnecter}" onclick="javascript:deconnecter()">Déconnexion</button>
<!-- زبانها -->
<div class="btn-group">
<button type="button" class="btn btn-danger" th:text="#{langues}">Langue</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')" th:text="#{langues.fr}" />
</li>
<li>
<a href="javascript:setLang('en')" th:text="#{langues.en}" />
</li>
</ul>
</div>
</form>
</div>
</div>
</div>
<!-- ابتداییسازی صفحه -->
<script th:inline="javascript">
/*<![CDATA[*/
// در حال راهاندازی صفحه
initNavBarRun();
/*]]>*/
</script>
</section>
این نما قالب ندارد. دارای رویدادپردازهای زیر است:
رویداد | پردازشگر |
روی دکمه خروج کلیک کنید | |
روی لینک [Français] کلیک کنید | |
روی لینک [English] کلیک کنید |
8.6.5.5. نما [accueil]
این نما بلافاصله زیر نوار ناوبری [navbar-run] نمایش داده میشود:

کد آن، [accueil.html]، به شرح زیر است:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info" th:text="#{choixmedecinjour.title}">Veuillez choisir un médecin et une date</div>
<div class="row">
<div class="col-md-3">
<h2 th:text="#{rv.medecin}">Médecin</h2>
<select name="idMedecin" id="idMedecin" class="combobox" data-style="btn-primary">
<option th:each="medecinItem : ${rdvmedecins.medecinItems}" th:text="${medecinItem.texte}" th:value="${medecinItem.id}"/>
</select>
</div>
<div class="col-md-3">
<h2 th:text="#{rv.jour}">Date</h2>
<section id="calendar_container">
<div id="calendar" class="input-group date">
<input id="displayjour" type="text" class="form-control btn-primary" disabled="true">
<span class="input-group-addon">
<i class="glyphicon glyphicon-th"></i>
</span>
</input>
</div>
</section>
</div>
</div>
<!-- تقویم -->
<div id="agenda"></div>
<!-- اسکریپت محلی -->
<script th:inline="javascript">
/*<![CDATA[*/
// ابتداییسازی صفحه
initChoixMedecinJour();
/*]]>*/
</script>
</html>
قالب آن به شرح زیر است:
- [rdvmedecins.medecinItems] (خط ۸): فهرست پزشکان؛
در شکل فعلی، به نظر نمیرسد که این نما هیچ رویدادپردازندهای داشته باشد. در واقع، این رویدادپردازندهها در تابع [initChoixMedecinJour] تعریف شدهاند. این تابع در بخش 8.6.4.7، در صفحهٔ 466 و بهطور دقیقتر در صفحهٔ 469 توضیح داده شده است. این تابع شامل رویدادپردازندههای زیر است:
رویداد | پردازشگر |
انتخاب یک پزشک | |
انتخاب یک تاریخ |
8.6.5.6. نما [agenda]
نما [agenda] یک روز از دفترچه یادداشت پزشک را نشان میدهد:

کد آن، [agenda.xml]، به شرح زیر است:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h3 class="alert alert-info" th:text="${agenda.titre}">Agenda de Mme Pélissier le 13/10/2014</h3>
<h4 class="alert alert-danger" th:if="${agenda.creneaux.length}==0" th:text="#{agenda.medecinsanscreneaux}">Ce médecin n'a pas encore de créneaux
de consultation</h4>
<th:block th:if="${agenda.creneaux.length}!=0">
<div class="row tab-content alert alert-warning">
<div class="tab-pane active col-md-6">
<table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span th:text="#{agenda.creneauhoraire}">Créneau horaire</span>
</th>
<th>
<span th:text="#{agenda.client}">Client</span>
</th>
<th data-hide="phone">
<span th:text="#{agenda.action}">Action</span>
</th>
</tr>
</thead>
<tbody>
<tr th:each="creneau,iter : ${agenda.creneaux}">
<td>
<span th:if="${creneau.action}==1" class="status-metro status-active" th:text="${creneau.creneauHoraire}">Créneau horaire</span>
<span th:if="${creneau.action}==2" class="status-metro status-suspended" th:text="${creneau.creneauHoraire}">Créneau horaire</span>
</td>
<td>
<span th:text="${creneau.client}">Client</span>
</td>
<td>
<a th:if="${creneau.action}==1" th:href="@{'javascript:reserverCreneau('+${creneau.id}+')'}" th:text="${creneau.commande}"
class="status-metro status-active">Réserver
</a>
<a th:if="${creneau.action}==2" th:href="@{'javascript:supprimerRv('+${creneau.idRv}+')'}" th:text="${creneau.commande}"
class="status-metro status-suspended">Supprimer
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- رزرو -->
<section th:include="resa" />
</th:block>
<!-- صفحهٔ اولیه -->
<script th:inline="javascript">
/*<![CDATA[*/
// ابتداییسازی صفحه
initAgenda();
/*]]>*/
</script>
</body>
</html>
قالب این نما تنها یک عنصر دارد:
- [agenda] (خط ۴): یک قالب نسبتاً پیچیده که بهطور خاص برای نمایش دفترچه خاطرات طراحی شده است؛
این رویدادپردازها را دارد:
رویداد | پردازشگر |
روی دکمه [Supprimer] کلیک کنید | |
روی لینک [Réserver] کلیک کنید |
نما [resa] در خط ۴۷ نمایی است که هنگام کلیک کاربر روی پیوند [Réserver] نمایش داده میشود:

کد آن، [resa.xml]، به شرح زیر است:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div id="resa" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">
</span>
</button>
<!-- <h4 class="modal-title">عنوان مودال -->
</div>
<div class="modal-body">
<div class="alert alert-info">
<h3>
<span th:text="#{resa.titre}">Prise de rendez-vous</span>
</h3>
</div>
<div class="row">
<div class="col-md-3">
<h2 th:text="#{resa.client}">Client</h2>
<select name="idClient" id="idClient" class="combobox" data-style="btn-primary">
<option th:each="clientItem : ${clientItems}" th:text="${clientItem.texte}" th:value="${clientItem.id}" />
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" onclick="javascript:cancelDialogResa()" th:text="#{resa.annuler}">Annuler</button>
<button type="button" class="btn btn-primary" onclick="javascript:validerRv()" th:text="#{resa.valider}">Valider</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- صفحهٔ اولیه -->
<script th:inline="javascript">
/*<![CDATA[*/
//صفحه راهاندازی شد
initResa();
/*]]>*/
</script>
</body>
</html>
قالب آن تنها شامل یک عنصر است:
- [clientItems] (خط ۲۴): فهرست مشتریان؛
این مدل شامل رویدادپردازهای زیر است:
رویداد | پردازشگر |
روی دکمه [Annuler] کلیک کنید | |
روی دکمه [Valider] کلیک کنید |
8.6.5.7. نما [erreurs]
این نما در صورتی نمایش داده میشود که عملی که کاربر درخواست کرده است، تکمیل نشده باشد:

کد [erreurs.xml] به شرح زیر است:
<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-danger">
<h4>
<span th:text="#{erreurs.titre}">Les erreurs suivantes se sont produites :</span>
</h4>
<ul>
<li th:each="message : ${erreurs}" th:text="${message}" />
</ul>
</div>
</section>
قالب آن تنها یک عنصر دارد:
- [erreurs] (خط ۸): فهرست خطاهایی که باید نمایش داده شوند؛
ویو هیچ رویدادپرداز (event handler) ندارد.
8.6.5.8. Résumé
جدول زیر نماها و قالبهای آنها را خلاصه میکند:
نما | مدل | پردازندههای رویداد |
navbar-start | ||
جَمبوترون | ||
ورود | ||
navbar-run | ||
خانه | ||
تقویم | ||
رزرو | ||
خطاها |
8.6.6. مرحله ۳: نوشتن اقدامات
بیایید به معماری سرویس وب [Web1] بازگردیم:
![]() |
اکنون به بررسی متدهای URL که توسط [Web1] ارائه شدهاند و پیادهسازی آنها میپردازیم:
8.6.6.1. عملیات URL که توسط سرویس [Web1] ارائه شدهاند
اینها به شرح زیر هستند:
- یک URL برای هر یک از نماهای قبلی یا ترکیبی از آنها؛
- یک URL برای افزودن یک RV؛
- یک URL برای حذف یک RV؛
همگی یک پاسخ از نوع [Reponse] را به شرح زیر بازمیگردانند:
public class Reponse {
// ----------------- ویژگیها
//وضعیت عملیات
private int status;
//نوار ناوبری
private String navbar;
//جومبوترون
private String jumbotron;
// بدنهٔ صفحه
private String content;
// تقویم
private String agenda;
...
}
- خط ۵: وضعیت پاسخ: ۱ (OK)، ۲ (خطا);
- خط ۷: جریان HTML از نماهای [navbar-start] یا [navbar-run]، بسته به مورد؛
- خط ۹: خوراک HTML از نما [jumbotron]؛
- خط ۱۳: جریان HTML از نمای [agenda]؛
- خط ۹: فید HTML از نماهای [accueil]، [erreurs] و [login]، حسب مورد؛
نمایشهای URL به شرح زیر است
نما [navbar-start] را در [Reponse.navbar] قرار میدهد | |
نما [navbar-run] را در [Reponse.navbar] قرار میدهد | |
نما [accueil] را در [Reponse.content] قرار میدهد | |
نما [jumbotron] را در [Reponse.jumbotron] قرار میدهد | |
نما [agenda] را در [Reponse.agenda] قرار میدهد | |
نما [login] را در [Reponse.content] قرار میدهد | |
| |
نما [navbar-run] را در [Reponse.navbar] قرار میدهد، نما [jumbotron] را در [Reponse.jumbotron]، نما [accueil] را در [Reponse.content]، نما [agenda] را در [Reponse.agenda] | |
قرار ملاقات انتخابشده را اضافه میکند و تقویم جدید را در [Reponse.agenda] قرار میدهد | |
قرار ملاقات انتخابشده را حذف میکند و تقویم جدید را در [Reponse.agenda] قرار میدهد |
8.6.6.2. سینگلتون [ApplicationModel]
![]() |
کلاس [ApplicationModel] به صورت یک نمونه واحد instantiate شده و به کنترلر برنامه تزریق میشود. کد آن به شرح زیر است:
package rdvmedecins.springthymeleaf.server.models;
import java.util.ArrayList;
...
@Component
public class ApplicationModel implements IDao {
....
}
- خط ۶: [ApplicationModel] یک کامپوننت Spring است؛
- خط ۷: که رابط لایه [DAO] را پیادهسازی میکند. ما این کار را انجام میدهیم تا اکشنها نیازی به آگاهی از لایه [DAO] نداشته باشند، بلکه فقط از singleton [ApplicationModel] آگاه باشند. معماری [Web1] سپس به شکل زیر درمیآید:
![]() |
بیایید به کد کلاس [ApplicationModel] بازگردیم:
package rdvmedecins.springthymeleaf.server.models;
import java.util.ArrayList;
...
@Component
public class ApplicationModel implements IDao {
//لایه [DAO]
@Autowired
private IDao dao;
//پیکربندی
@Autowired
private AppConfig appConfig;
//دادهها از لایه [DAO]
private List<ClientItem> clientItems;
private List<MedecinItem> medecinItems;
//دادههای پیکربندی
private String userInit;
private String mdpUserInit;
private boolean corsAllowed;
// استثناء
private RdvMedecinsException rdvMedecinsException;
// تولیدکننده
public ApplicationModel() {
}
@PostConstruct
public void init() {
// پیکربندی
userInit = appConfig.getUSER_INIT();
mdpUserInit = appConfig.getMDP_USER_INIT();
dao.setTimeout(appConfig.getTIMEOUT());
dao.setUrlServiceWebJson(appConfig.getWEBJSON_ROOT());
corsAllowed = appConfig.isCORS_ALLOWED();
// کش کردن لیستهای کشویی برای پزشکان و مشتریان
List<Medecin> medecins = null;
List<Client> clients = null;
try {
medecins = dao.getAllMedecins(new User(userInit, mdpUserInit));
clients = dao.getAllClients(new User(userInit, mdpUserInit));
} catch (RdvMedecinsException ex) {
rdvMedecinsException = ex;
}
if (rdvMedecinsException == null) {
// موارد لیست کشویی ایجاد میشوند
medecinItems = new ArrayList<MedecinItem>();
for (Medecin médecin : medecins) {
medecinItems.add(new MedecinItem(médecin));
}
clientItems = new ArrayList<ClientItem>();
for (Client client : clients) {
clientItems.add(new ClientItem(client));
}
}
}
// گیرندهها و تنظیمکنندهها
...
//پیادهسازی رابط [IDao]
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
@Override
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
return dao.ajouterRv(user, jour, idCreneau, idClient);
}
...
}
- خط ۱۱: تزریق مرجع به پیادهسازی لایه [DAO]. سپس این مرجع است که برای پیادهسازی رابط [IDao] (خطوط ۶۴–۸۰) استفاده میشود؛
- خط ۱۴: تزریق پیکربندی برنامه؛
- خطوط ۳۳–۳۷: این پیکربندی برای پیکربندی عناصر مختلف معماری برنامه استفاده میشود؛
- خطوط ۳۸–۴۶: اطلاعاتی که لیستهای کشویی پزشکان و مشتریان را پر میکند، در حافظه پنهان ذخیره میشود. بنابراین فرض میکنیم که اگر پزشکی یا مشتری تغییر کند، باید برنامه را مجدداً راهاندازی کرد. هدف از این کار نشان دادن این است که یک کلاس تکنسخه (singleton) در Spring میتواند به عنوان حافظه پنهان برای برنامه وب عمل کند؛
کلاسهای [MedecinItem] و [ClientItem] هر دو از کلاس زیر، [PersonneItem]، ارث میبرند:
package rdvmedecins.springthymeleaf.server.models;
import rdvmedecins.client.entities.Personne;
public class PersonneItem {
// مورد لیست
private Long id;
private String texte;
// سازنده
public PersonneItem() {
}
public PersonneItem(Personne personne) {
id = personne.getId();
texte = String.format("%s %s %s", personne.getTitre(), personne.getPrenom(), personne.getNom());
}
// گیرندهها و تنظیمکنندهها
...
}
- خط ۸: فیلد [id] مقدار ویژگی [value] برای یک گزینه در لیست کشویی خواهد بود؛
- خط ۹: فیلد [texte] متنی است که برای یک گزینه در لیست کشویی نمایش داده میشود؛
8.6.6.3. کلاس [BaseController]
![]() |
کلاس [BaseController] کلاس والد کنترلکنندههای [RdvMedecinsController] و [RdvMedecinsCorsController] است. ایجاد این کلاس والد ضروری نبود. این کلاس شامل متدهای کمکی از کلاس [RdvMedecinsController] است که هیچکدام جز یکی ضروری نیستند. این متدها را میتوان به سه گروه تقسیم کرد:
- متدهای کمکی؛
- متدهایی که ویوها را با مدلهایشان ادغام میکنند؛
- متد برای راهاندازی یک اکشن
| دو متد کاربردی که فهرستی از پیامهای خطا را ارائه میدهند. ما قبلاً با آنها برخورد کرده و از آنها استفاده کردهایم؛ |
| نما [accueil] را بدون قالب رندر میکند |
| نما [agenda] و قالب آن را بازمیگرداند |
| نما [login] را بدون قالب بازمیگرداند |
| پاسخ را به کلاینت بازمیگرداند وقتی که عمل درخواستی با خطا پایان یافته است |
| روش راهاندازی تمام عملیات کنترلر [RdvMedecinsController] |
بیایید دو تا از این متدها را بررسی کنیم.
متد [getPartialViewAgenda] پیچیدهترین نما، یعنی نمای تقویم را تولید میکند. کد آن به شرح زیر است:
//جریان [agenda]
protected String getPartialViewAgenda(ActionContext actionContext, AgendaMedecinJour agenda, Locale locale) {
// زمینهها
WebContext thymeleafContext = actionContext.getThymeleafContext();
WebApplicationContext springContext = actionContext.getSpringContext();
// ما قالب صفحه را میسازیم [agenda]
ViewModelAgenda modelAgenda = setModelforAgenda(agenda, springContext, locale);
// تقویم با قالب خود
thymeleafContext.setVariable("agenda", modelAgenda);
thymeleafContext.setVariable("clientItems", application.getClientItems());
return engine.process("agenda", thymeleafContext);
}
- خطوط ۹–۱۰: دو عنصر قالب تقویم:
- خط ۹: تقویم نمایش داده شده.
- خط ۱۰: فهرست مشتریانی که هنگام تعیین وقت ملاقات توسط کاربر نمایش داده میشود؛
متد [setModelforAgenda] در خط ۷ به شرح زیر است:
//قالب صفحه [Agenda]
private ViewModelAgenda setModelforAgenda(AgendaMedecinJour agenda, WebApplicationContext springContext, Locale locale) {
// عنوان صفحه
String dateFormat = springContext.getMessage("date.format", null, locale);
Medecin médecin = agenda.getMedecin();
String titre = springContext.getMessage("agenda.titre", new String[] { médecin.getTitre(), médecin.getPrenom(),
médecin.getNom(), new SimpleDateFormat(dateFormat).format(agenda.getJour()) }, locale);
// اسلاتهای قرار ملاقات
ViewModelCreneau[] modelCréneaux = new ViewModelCreneau[agenda.getCreneauxMedecinJour().length];
int i = 0;
for (CreneauMedecinJour creneauMedecinJour : agenda.getCreneauxMedecinJour()) {
// ساعت ملاقات با پزشک
Creneau créneau = creneauMedecinJour.getCreneau();
ViewModelCreneau modelCréneau = new ViewModelCreneau();
modelCréneaux[i] = modelCréneau;
//شناسه
modelCréneau.setId(créneau.getId());
// زمانبندی
modelCréneau.setCreneauHoraire(String.format("%02dh%02d-%02dh%02d", créneau.getHdebut(), créneau.getMdebut(),
créneau.getHfin(), créneau.getMfin()));
Rv rv = creneauMedecinJour.getRv();
// مشتری و سفارش
String commande;
if (rv == null) {
modelCréneau.setClient("");
commande = springContext.getMessage("agenda.reserver", null, locale);
modelCréneau.setCommande(commande);
modelCréneau.setAction(ViewModelCreneau.ACTION_RESERVER);
} else {
Client client = rv.getClient();
modelCréneau.setClient(String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom()));
commande = springContext.getMessage("agenda.supprimer", null, locale);
modelCréneau.setCommande(commande);
modelCréneau.setIdRv(rv.getId());
modelCréneau.setAction(ViewModelCreneau.ACTION_SUPPRIMER);
}
// اسلات بعدی
i++;
}
//قالب دفترچه یادداشت بازگردانده میشود
ViewModelAgenda modelAgenda = new ViewModelAgenda();
modelAgenda.setTitre(titre);
modelAgenda.setCreneaux(modelCréneaux);
return modelAgenda;
}
- خط ۶: تقویم دارای عنوان است:

یا:

میتوانیم ببینیم که فرمت تاریخ به زبان بستگی دارد. ما این فرمت را از فایلهای پیام (خط ۴) بازیابی خواهیم کرد.
- خطوط ۱۱–۴۰: برای هر بازه زمانی، باید نما را نمایش دهیم:
![]()
یا نما:
![]()
- خطوط ۱۹–۲۰: نمایش شکاف زمانی؛
- خطوط ۲۵–۲۸: زمانی که شکاف زمانی آزاد است. در این حالت، دکمه [Réserver] باید نمایش داده شود؛
- خطوط ۳۱–۳۶: حالتی که بازه زمانی رزرو شده است. در این حالت، هم مشتری و هم دکمه [Supprimer] باید نمایش داده شوند؛
روش دیگر، که به تفصیل توضیح میدهیم، روش [getActionContext] است. این روش در ابتدای هر یک از عملیاتهای [RdvMedecinsController] فراخوانی میشود. امضای آن به شرح زیر است:
protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController)
این تابع نوع زیر را برمیگرداند، [ActionContext]:
public class ActionContext {
// دادهها
private WebContext thymeleafContext;
private WebApplicationContext springContext;
private Locale locale;
private List<String> erreurs;
...
}
- خط ۴: زمینه Thymeleaf برای اکشن؛
- خط ۵: کانکست Spring برای اکشن؛
- خط ۶: لوکال اکشن؛
- خط ۷: فهرستی از پیامهای خطا، در صورت وجود؛
پارامترهای آن به شرح زیر است:
- [lang]: زبان درخواستشده برای اقدام 'en' یا 'fr' است؛
- [origin]: هدر HTTP [origin] در مورد فراخوانی بیندامنهای؛
- [request]: درخواست HTTP که در حال پردازش است، که مدتی است به عنوان یک اقدام (action) نامیده میشود؛
- [response]: پاسخی که برای این درخواست تولید خواهد شد؛
- [result]: هر اکشن در [RdvMedecinsController] یک مقدار ارسالشده (posted) دریافت میکند که اعتبار آن بررسی میشود. [result] نتیجه این آزمون است؛
- [rdvMedecinsController]: کنترلکنندهای که شامل اکشنها است؛
روش [getActionContext] به شرح زیر پیادهسازی شده است:
// زمینه یک عمل
protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController) {
// زبان؟
if (lang == null) {
lang = "fr";
}
// محلی
Locale locale = null;
if (lang.trim().toLowerCase().equals("fr")) {
// فرانسوی
locale = new Locale("fr", "FR");
} else {
// بقیه موارد به زبان انگلیسی
locale = new Locale("en", "US");
}
// سربرگها CORS
rdvMedecinsCorsController.sendOptions(origin, response);
// ActionContext
ActionContext actionContext = new ActionContext(new WebContext(request, response, request.getServletContext(),locale), WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()), locale, null);
//خطاهای инициализация
RdvMedecinsException e = application.getRdvMedecinsException();
if (e != null) {
actionContext.setErreurs(e.getMessages());
return actionContext;
}
// خطاها در POST؟
if (result != null && result.hasErrors()) {
actionContext.setErreurs(getErreursForModel(result, locale, actionContext.getSpringContext()));
return actionContext;
}
// بدون خطا
return actionContext;
}
- خطوط ۳–۱۵: با استفاده از پارامتر [lang]، لوکال اقدام تنظیم میشود؛
- خط 17: سربرگهای HTTP مورد نیاز برای درخواستهای بیندامنهای ارسال میشوند. در اینجا به جزئیات نمیپردازیم. تکنیک مورد استفاده همان است که در بند 8.4.14 توضیح داده شده است؛
- خط ۱۹: ساخت یک شیء [ActionContext] بدون خطا؛
- خط ۲۱: همانطور که در بخش ۸.۶.۶.۲ دیدیم، singleton (اشیاء یکتا) [ApplicationModel] برای بازیابی هر دو مشتری و پزشک به پایگاه داده دسترسی پیدا میکند. این دسترسی ممکن است با شکست مواجه شود. بنابراین ما استثناء حاصل را ثبت میکنیم. در خط ۲۱، ما این استثناء را بازیابی میکنیم؛
- خطوط ۲۲–۲۵: اگر هنگام راهاندازی برنامه یک استثنا رخ داده باشد، هیچ اقدامی ممکن نیست. بنابراین ما یک شیء [ActionContext] حاوی پیامهای خطای استثنا را برای هرگونه اقدام بازمیگردانیم؛
- خطوط 27–20: ما پارامتر [result] را تحلیل میکنیم تا مشخص کنیم که آیا مقدار ارسالشده معتبر بوده است یا خیر. اگر نامعتبر باشد، یک شیء [ActionContext] حاوی پیامهای خطای مناسب را بازمیگردانیم؛
- خط ۳۲: حالت بدون خطا؛
اکنون اقدامات کنترلکننده [RdvMedecinsController] را بررسی خواهیم کرد
8.6.6.4. اقدام [/getNavBarStart]
عمل [/getNavBarStart] نما [navbar-start] را بازمیگرداند. امضای آن به شرح زیر است:
@RequestMapping(value = "/getNavbarStart", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarStart(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
این نوع زیر را برمیگرداند: [Reponse]:
public class Reponse {
// ----------------- ویژگیها
//وضعیت تراکنش
private int status;
//نوار ناوبری
private String navbar;
//جومبوترون
private String jumbotron;
// بدنهٔ صفحه
private String content;
// تقویم
private String agenda;
...
}
و دارای پارامترهای زیر است:
- [PostLang postlang]: مقدار ارسالشده زیر:
public class PostLang {
// دادهها
@NotNull
private String lang;
...
}
کلاس [PostLang] کلاس والد تمام مقادیر ارسالشده است. این به این دلیل است که کلاینت باید همیشه زبان مورد استفاده برای اجرای عملیات را مشخص کند.
متد [getNavbarStart] به شرح زیر پیادهسازی شده است:
// navbar-start
@RequestMapping(value = "/getNavbarStart", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarStart(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// زمینههای اقدام
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// خطاها؟
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// نما نمایش داده میشود [navbar-start]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-start", thymeleafContext));
return reponse;
}
- خط ۷: инициалиزاسيون اقدام؛
- خطوط ۱۰–۱۳: اگر متد инициализация اقدام خطایی گزارش کرده باشد، این خطاها در پاسخ (خط ۱۲) با وضعیت ۲ به کلاینت بازگردانده میشوند:
- خطوط ۱۵–۱۸: نمای [navbar-start] با وضعیت ۱ ارسال میشود:
در ادامه، تنها ویژگیهای جدید را توضیح خواهیم داد.
8.6.6.5. عمل [/getNavbarRun]
عمل [/getNavBarRun] نما [navbar-run] را بازمیگرداند:
// navbar-run
@RequestMapping(value = "/getNavbarRun", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarRun(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// زمینههای اقدام
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
//خطاها؟
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// نما بازگشت داده میشود: [navbar-run]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
return reponse;
}
این اقدام میتواند دو نوع پاسخ بازگرداند:
- پاسخ خطا (خطوط ۱۰–۱۳):
- پاسخ با نما [navbar-run]:
8.6.6.6. عمل [/getJumbotron]
عمل [/getJumbotron] نما [jumbotron] را بازمیگرداند:
// جومبوترون
@RequestMapping(value = "/getJumbotron", method = RequestMethod.POST)
@ResponseBody
public Reponse getJumbotron(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// زمینههای اقدام
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
//خطاها؟
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// نما را بازمیگرداند [jumbotron]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
return reponse;
}
اقدام میتواند دو نوع پاسخ بازگرداند:
- پاسخ خطا (خطوط ۱۰–۱۳):
- پاسخ با نما [jumbotron]:
8.6.6.7. عمل [/getLogin]
عمل [/getLogin] نما [login] را بازمیگرداند:
@RequestMapping(value = "/getLogin", method = RequestMethod.POST)
@ResponseBody
public Reponse getLogin(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// زمینههای اقدام
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
//خطاها؟
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// نما را بازمیگرداند [login]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setNavbar(engine.process("navbar-start", thymeleafContext));
reponse.setContent(getPartialViewLogin(thymeleafContext));
return reponse;
}
اقدام میتواند دو نوع پاسخ بازگرداند:
- پاسخ خطا (خطوط ۹–۱۱):
- پاسخ با نما [login]:
8.6.6.8. عمل [/getAccueil]
عمل [/getAccueil] نما [accueil] را بازمیگرداند. امضای آن به شرح زیر است:
@RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
@ResponseBody
public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin)
- در خط ۳، مقدار ارسالشده از نوع [PostUser] است، به شرح زیر:
public class PostUser extends PostLang {
// دادهها
@NotNull
private User user;
...
}
- خط ۱: کلاس [PostUser] از کلاس [PostLang] ارث میبرد و بنابراین شامل یک زبان است؛
- خط ۴: کاربر در تلاش برای بازیابی نما است؛
کد پیادهسازی به شرح زیر است:
@RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
@ResponseBody
public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// زمینههای اقدام
ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// خطاها؟
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// نما [accueil] محافظت شده است
try{
// کاربر
User user = postUser.getUser();
//اعتبارنامهها در حال تأیید هستند [userName, password]
application.authenticate(user);
}catch(RdvMedecinsException e){
//خطایی بازگردانده میشود
return getViewErreurs(thymeleafContext, e.getMessages());
}
//نما بازگردانده میشود [accueil]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setContent(getPartialViewAccueil(thymeleafContext));
return reponse;
}
- خطوط ۱۵–۲۲: توجه داشته باشید که صفحه [accueil] محافظت شده است و بنابراین کاربر باید احراز هویت شود؛
عمل میتواند دو نوع پاسخ بازگرداند:
- پاسخ خطا (خطوط ۱۱ و ۲۱):
- پاسخ با نما [accueil] (خطوط 24–27):
8.6.6.9. اقدام [/getNavbarRunJumbotronAccueil]
عمل [/getNavbarRunJumbotronAccueil] نماهای [navbar-run, jumbotron, accueil] را تولید میکند. این عمل دارای امضای زیر است:
@RequestMapping(value = "/getNavbarRunJumbotronAccueil", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueil(@Valid @RequestBody PostUser post, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
- خط ۳: مقدار ارسالشده از نوع [PostUser] است؛
پیادهسازی این عمل به شرح زیر است:
// navbar + jumbotron + home
@RequestMapping(value = "/getNavbarRunJumbotronAccueil", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// زمینههای اقدام
ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,
rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
//خطاها؟
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// نما [accueil] محافظت شده است
try {
// کاربر
User user = postUser.getUser();
// اعتبارنامهها در حال تأیید هستند [userName, password]
application.authenticate(user);
} catch (RdvMedecinsException e) {
//خطایی بازگردانده میشود
return getViewErreurs(thymeleafContext, e.getMessages());
}
// پاسخ ارسال میشود
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setContent(getPartialViewAccueil(thymeleafContext));
return reponse;
}
این اقدام میتواند دو نوع پاسخ بازگرداند:
- پاسخ خطا (خطوط ۱۳، ۲۳):
- پاسخی با دیدگاههای [navbar-run, jumbotron, accueil] (خطوط 26–31):
8.6.6.10. عمل [/getAgenda]
عمل [/getAgenda] نما [agenda] را بازمیگرداند. امضای آن به شرح زیر است:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
- خط ۳: مقدار ارسالشده از نوع [PostGetAgenda] است، به شرح زیر:
public class PostGetAgenda extends PostUser {
// دادهها
@NotNull
private Long idMedecin;
@NotNull
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date jour;
...
}
- خط ۱: کلاس [PostGetAgenda] از کلاس [PostUser] ارث میبرد و بنابراین شامل یک زبان و یک کاربر است؛
- خط ۵: شمارهٔ دکتر که برای دفترچهٔ او تقویم میخواهید؛
- خط ۸: روز مورد نیاز برای یادداشت دفترچه خاطرات؛
پیادهسازی به شرح زیر است:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// زمینههای اقدام
ActionContext actionContext = getActionContext(postGetAgenda.getLang(), origin, request, response, result, rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
WebApplicationContext springContext = actionContext.getSpringContext();
Locale locale = actionContext.getLocale();
// خطاها؟
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// اعتبار درخواست POST بررسی میشود
if (result != null) {
new PostGetAgendaValidator().validate(postGetAgenda, result);
if (result.hasErrors()) {
// بازگشت نما [erreurs]
return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
}
}
...
}
- تا خط ۱۴، کد اکنون استاندارد است؛
- خطوط ۱۶–۲۱: یک بررسی اضافی روی مقدار ارسالشده انجام میشود. تاریخ باید برابر یا بعد از تاریخ امروز باشد. برای تأیید این موضوع، از یک اعتبارسنج استفاده میشود:
package rdvmedecins.web.validators;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import rdvmedecins.springthymeleaf.server.requests.PostGetAgenda;
import rdvmedecins.springthymeleaf.server.requests.PostValiderRv;
public class PostGetAgendaValidator implements Validator {
public PostGetAgendaValidator() {
}
@Override
public boolean supports(Class<?> classe) {
return PostGetAgenda.class.equals(classe) || PostValiderRv.class.equals(classe);
}
@Override
public void validate(Object post, Errors errors) {
// روز انتخابشده برای قرار ملاقات
Date jour = null;
if (post instanceof PostGetAgenda) {
jour = ((PostGetAgenda) post).getJour();
} else {
if (post instanceof PostValiderRv) {
jour = ((PostValiderRv) post).getJour();
}
}
//تبدیل تاریخها به فرمت yyyy-MM-dd
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String strJour = sdf.format(jour);
String strToday = sdf.format(new Date());
// روز انتخابشده نباید زودتر از تاریخ امروز باشد
if (strJour.compareTo(strToday) < 0) {
errors.rejectValue("jour", "todayandafter.postChoixMedecinJour", null, null);
}
}
}
- خط ۱۹: اعتبارسنج روی دو کلاس عمل میکند: [PostGetAgenda] و [PostValiderRv];
بیایید به کد مربوط به اقدام [/getAgenda] بازگردیم:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
...
// اقدام
try {
//دفترچه یادداشت پزشک
AgendaMedecinJour agenda = application.getAgendaMedecinJour(postGetAgenda.getUser(), postGetAgenda.getIdMedecin(),
new SimpleDateFormat("yyyy-MM-dd").format(postGetAgenda.getJour()));
// پاسخ
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException e1) {
// نما [erreurs] بازگردانده میشود
return getViewErreurs(thymeleafContext, e1.getMessages());
} catch (Exception e2) {
// نما را بازمیگرداند [erreurs]
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
- خطوط ۹–۱۰: با استفاده از پارامترهای ارسالشده، دفترچه یادداشت پزشک را درخواست میکنیم؛
- خطوط ۱۲–۱۳: برنامه زمانبندی بازگردانده میشود:
- خطوط 17 و 21: یک پاسخ خطا بازگردانده میشود:
8.6.6.11. عمل [/getNavbarRunJumbotronAccueilAgenda]
عمل [/getNavbarRunJumbotronAccueilAgenda] نماهای [navbar-run, jumbotron, accueil, agenda] را بازمیگرداند. پیادهسازی آن به شرح زیر است:
@RequestMapping(value = "/getNavbarRunJumbotronAccueilAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueilAgenda(@Valid @RequestBody PostGetAgenda post, BindingResult result,
HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// زمینههای اقدام
ActionContext actionContext = getActionContext(post.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
//خطاها؟
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// تقویم
Reponse agenda = getAgenda(post, result, request, response, null);
if (agenda.getStatus() != 1) {
return agenda;
}
//پاسخ ارسال میشود
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setContent(getPartialViewAccueil(thymeleafContext));
reponse.setAgenda(agenda.getAgenda());
return reponse;
}
- خطوط ۱۵–۱۸: ما از اکشن [/getAgenda] برای فراخوانی آن استفاده میکنیم. سپس status را در پاسخ بررسی میکنیم (خط ۱۶). اگر خطایی تشخیص داده شود، در آنجا متوقف شده و پاسخ را بازمیگردانیم؛
- خط ۲۰: ما نماهای درخواستی را ارسال میکنیم:
8.6.6.12. اقدام [/supprimerRv]
عمل [/supprimerRv] امکان حذف یک قرار ملاقات را فراهم میکند. امضای آن به شرح زیر است:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
- خط ۳: مقدار ارسالشده از نوع [PostSupprimerRv] است، به شرح زیر:
public class PostSupprimerRv extends PostUser {
// دادهها
@NotNull
private Long idRv;
..
}
- خط ۱: کلاس [PostSupprimerRv] از کلاس [PostUser] ارث میبرد و بنابراین شامل یک زبان و یک کاربر است؛
- خط ۵: شماره قرار ملاقاتی که باید حذف شود؛
پیادهسازی این عمل به شرح زیر است:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// زمینههای اقدام
ActionContext actionContext = getActionContext(postSupprimerRv.getLang(), origin, request, response, result,
rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
Locale locale = actionContext.getLocale();
// خطاها؟
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// ارزشهای ارسالشده
User user = postSupprimerRv.getUser();
long idRv = postSupprimerRv.getIdRv();
// قرار ملاقات حذف میشود
AgendaMedecinJour agenda = null;
try {
//بازیابی آن
Rv rv = application.getRvById(user, idRv);
Creneau creneau = application.getCreneauById(user, rv.getIdCreneau());
long idMedecin = creneau.getIdMedecin();
Date jour = rv.getJour();
// قرار ملاقات مرتبط حذف شد
application.supprimerRv(user, idRv);
// بازتولید دفترچه یادداشت پزشک
agenda = application.getAgendaMedecinJour(user, idMedecin, new SimpleDateFormat("yyyy-MM-dd").format(jour));
// بازگرداندن تقویم جدید
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException ex) {
// نما را بازمیگرداند [erreurs]
return getViewErreurs(thymeleafContext, ex.getMessages());
} catch (Exception e2) {
// بازگشت نما [erreurs]
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
- خط ۲۲: قرار ملاقاتی که باید حذف شود، بازیابی میشود. اگر وجود نداشته باشد، یک استثنا پرتاب میشود؛
- خطوط ۲۳–۲۵: از این قرار ملاقات، پزشک و روز مربوطه شناسایی میشوند. این اطلاعات برای بازتولید دفترچه یادداشت پزشک مورد نیاز است؛
- خط ۲۷: قرار ملاقات حذف میشود؛
- خط ۲۹: دفترچه یادداشت بهروزرسانیشده پزشک درخواست میشود. این مهم است. علاوه بر زمانی که به تازگی آزاد شده است، ممکن است سایر کاربران برنامه تغییراتی در دفترچه یادداشت ایجاد کرده باشند. مهم است که جدیدترین نسخه دفترچه یادداشت به کاربر بازگردانده شود؛
- ردههای ۳۱–۳۴: تقویم بازگردانده میشود:
8.6.6.13. عمل [/validerRv]
عمل [/validerRv] یک قرار ملاقات را به تقویم پزشک اضافه میکند. امضای آن به شرح زیر است:
@RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin)
- خط ۳: مقدار ارسالشده از نوع [PostValiderRv] به شرح زیر است:
public class PostValiderRv extends PostUser {
//دادهها
@NotNull
private Long idCreneau;
@NotNull
private Long idClient;
@NotNull
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date jour;
...
}
- خط ۱: کلاس [PostValiderRv] از کلاس [PostUser] ارث میبرد و بنابراین شامل یک زبان و یک کاربر است؛
- خط ۵: شماره بازه زمانی؛
- خط ۷: شماره مشتری که برای او رزرو انجام شده است؛
- خط ۱۰: روز قرار ملاقات؛
پیادهسازی این اقدام به شرح زیر است:
//تأیید قرار ملاقات
@RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// زمینههای اقدام
ActionContext actionContext = getActionContext(postValiderRv.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebApplicationContext springContext = actionContext.getSpringContext();
WebContext thymeleafContext = actionContext.getThymeleafContext();
Locale locale = actionContext.getLocale();
//خطاها؟
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
//بررسی اعتبار تاریخ قرار ملاقات
if (result != null) {
new PostGetAgendaValidator().validate(postValiderRv, result);
if (result.hasErrors()) {
// نما [erreurs] را بازمیگرداند
return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
}
}
//مقادیر ارسالشده
User user = postValiderRv.getUser();
long idClient = postValiderRv.getIdClient();
long idCreneau = postValiderRv.getIdCreneau();
Date jour = postValiderRv.getJour();
// اقدام
try {
//بازیابی اطلاعات درباره بازه زمانی
Creneau créneau = application.getCreneauById(user, idCreneau);
long idMedecin = créneau.getIdMedecin();
// افزودن قرار ملاقات
application.ajouterRv(postValiderRv.getUser(), new SimpleDateFormat("yyyy-MM-dd").format(jour), idCreneau,idClient);
// تولید مجدد تقویم
AgendaMedecinJour agenda = application.getAgendaMedecinJour(user, idMedecin,
new SimpleDateFormat("yyyy-MM-dd").format(jour));
// بازگرداندن تقویم جدید
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException ex) {
// بازگرداندن نما [erreurs]
return getViewErreurs(thymeleafContext, ex.getMessages());
} catch (Exception e2) {
// بازگرداندن نما [erreurs]
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
}
این کد مشابه کد اکشن [/supprimerRv] است.
8.6.7. مرحله ۴: آزمایش سرور Spring/Thymeleaf
اکنون ما با استفاده از افزونه کروم [Advanced Rest Client] (به بخش 9.6 مراجعه کنید) اقدامات مختلف توصیفشده در بالا را آزمایش خواهیم کرد.
8.6.7.1. پیکربندی تست
تمام عملیات منتظر یک مقدار ارسالشده هستند. ما نسخههای مختلفی از رشته زیر را ارسال خواهیم کرد: jSON:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
این مقدار ارسالشده شامل اطلاعاتی است که برای اکثر عملیات اضافی است. با این حال، این اطلاعات توسط عملیات دریافتکننده نادیده گرفته میشود و خطایی ایجاد نمیکند. این مقدار ارسالشده مزیت پوشش انواع مقادیری را که باید ارسال شوند دارد.
8.6.7.2. عمل [/getNavbarStart]
![]() |
- به [1]، عملی که در حال آزمایش است؛
- به [2]، مقدار ارسالشده؛
- در [3]، مقدار ارسالشده یک رشته jSON است؛
- در [4]، نمای [navbar-start] به زبان انگلیسی درخواست شده است؛
نتیجهٔ بهدستآمده به شرح زیر است:
![]() |
ما نما [navbar-start] را به زبان انگلیسی دریافت کردیم (میدانها برجسته شدهاند).
حالا بیایید یک اشتباه انجام دهیم. ما مقدار ویژگی [lang] را از مقدار ارسالشده به null تغییر میدهیم. نتیجه زیر را دریافت میکنیم:
![]() |
ما یک پاسخ خطا (وضعیت 2) دریافت کردیم که نشان میداد فیلد [lang] اجباری است.
8.6.7.3. اقدام [/getNavbarRun]
ما اقدام [getNavbarRun] را با مقدار ارسالشده زیر درخواست میکنیم:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
نتیجهٔ بهدستآمده به شرح زیر است:
![]() |
8.6.7.4. اقدام [/getJumbotron]
ما در حال درخواست اقدام [getJumbotron] با مقدار ارسالشده زیر هستیم:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
نتیجهٔ بهدستآمده به شرح زیر است:
![]() |
8.6.7.5. اقدام [/getLogin]
ما در حال درخواست اقدام [getLogin] با مقدار ارسالشده زیر هستیم:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
نتیجه بهدستآمده به شرح زیر است:
![]() |
8.6.7.6. اقدام [/getAccueil]
ما در حال درخواست اقدام [getAccueil] با مقدار ارسالشده زیر هستیم:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
نتیجهٔ بهدستآمده به شرح زیر است:
![]() |
ما دوباره با یک کاربر ناشناس تلاش میکنیم:
{"user":{"login":"x","passwd":"x"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
نتیجه به شرح زیر است:
![]() |
بیایید دوباره با یک کاربر موجود که مجاز به استفاده از برنامه نیست، امتحان کنیم:
{"user":{"login":"user","passwd":"user"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
نتیجه به شرح زیر است:
![]() |
8.6.7.7. عمل [/getAgenda]
ما اقدام [getAgenda] را با مقدار ارسالشده زیر درخواست میکنیم:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
نتیجهٔ بهدستآمده به شرح زیر است:
![]() |
ما دوباره با تاریخی زودتر از امروز تلاش میکنیم:
![]() |
بیایید دوباره با یک دکتر خیالی امتحان کنیم:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":11, "idCreneau":2, "idClient":4, "idRv":93}
نتیجهٔ بهدستآمده به شرح زیر است:
![]() |
8.6.7.8. عمل [/getNavbarRunJumbotronAccueil]
ما اقدام [getNavbarRunJumbotronAccueil] را با مقدار ارسالشده زیر درخواست میکنیم:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
نتیجهٔ بهدستآمده به شرح زیر است:
![]() |
همین امر برای یک کاربر ناشناس نیز صدق میکند:
![]() |
8.6.7.9. عمل [/getNavbarRunJumbotronAccueilAgenda]
ما اقدام [getNavbarRunJumbotronAccueilAgenda] را با مقدار ارسالشده زیر درخواست میکنیم:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
نتیجهٔ بهدستآمده به شرح زیر است:
![]() |
ما یک دکتر را وارد میکنیم که وجود ندارد:
![]() |
8.6.7.10. اقدام [/supprimerRv]
ما اقدام [supprimerRv] را با مقدار ارسالشده زیر درخواست میکنیم:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
شماره مرجع 93 وجود ندارد. نتیجه حاصل به شرح زیر است:
![]() |
با یک قرار ملاقات که وجود دارد:
![]() |
میتوان در پایگاه داده تأیید کرد که قرار ملاقات واقعاً حذف شده است. تقویم جدید بازگردانده میشود.
8.6.7.11. عمل [/validerRv]
ما در حال درخواست اقدام [validerRv] با مقدار ثبتشده زیر هستیم:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
نتیجهٔ بهدستآمده به شرح زیر است:
![]() |
میتوانیم در پایگاه داده بررسی کنیم که قرار ملاقات واقعاً ایجاد شده است. تقویم جدید بازگردانده شده است.
ما همین کار را با یک شماره اسلات غیرموجود نیز انجام میدهیم:
![]() |
ما همین کار را با یک شماره مشتری ناموجود انجام میدهیم:
![]() |
8.6.8. مرحله ۵: نوشتن کلاینت جاوااسکریپت
بیایید به معماری سرور [Web1] بازگردیم:
![]() |
کلاینت [2] سرور [Web1] یک کلاینت جاوااسکریپت از نوع APU (برنامهٔ تکصفحهای) است:
- کلاینت صفحهٔ بوت را از یک وبسرور (نه لزوماً [Web1]) درخواست میکند؛
- صفحات بعدی را از طریق فراخوانیهای Ajax از سرور [Web1] درخواست میکند؛
برای ساخت این کلاینت، از ابزار [Webstorm] استفاده خواهیم کرد (به بخش 9.8 مراجعه کنید). من این ابزار را کاربردیتر از STS یافتم. مزیت اصلی آن این است که تکمیل خودکار کد و همچنین برخی از گزینههای موجود در refactoring را ارائه میدهد. این امر به جلوگیری از بسیاری از خطاها کمک میکند.
8.6.8.1. پروژه JS
پروژه JS ساختار دایرکتوری زیر را دارد:
![]() |
- در [1]، کلاینت JS به طور کامل. [boot.html] صفحه شروع است. این تنها صفحهای خواهد بود که توسط مرورگر بارگیری میشود؛
- [2] حاوی فایلهای سبک برای کامپوننتهای Bootstrap است؛
- در [3]، چند تصویر مورد استفاده توسط برنامه؛
![]() |
- در [4]، اسکریپتهای JS. اینجاست که کار ما آغاز میشود؛
- در [5]، کتابخانههای JS مورد استفاده: عمدتاً jQuery و کتابخانههای مربوط به کامپوننتهای Bootstrap؛
8.6.8.2. معماری کد
کد به سه لایه تقسیم شده است:
![]() |
- لایه [présentation] شامل توابع инициализация برای صفحه [boot.xml] و همچنین توابع مربوط به کامپوننتهای مختلف Bootstrap است. این لایه توسط فایل [ui.js] پیادهسازی شده است؛
- لایه [événements] شامل تمام رویدادپردازها از لایه [présentation] است. این لایه توسط فایل [evts.js] پیادهسازی شده است؛
- لایه [DAO] درخواستهای HTTP را به سرور [Web1] ارسال میکند. این لایه توسط فایل [dao.js] پیادهسازی شده است؛
8.6.8.3. لایه [présentation]
![]() |
لایه [présentation] توسط فایل زیر [ui.js] پیادهسازی شده است:
//لایه [présentation]
var ui = {
// متغیرهای سراسری؛
"agenda": "",
"resa": "",
"langue": "",
"urlService": "http://localhost:8081",
"page": "login",
"jourAgenda": "",
"idMedecin": "",
"user": {},
"login": {},
"exceptionTitle": {},
"calendar_infos": {},
"erreur": "",
"idCreneau": "",
"done": "",
// اجزای نما
"body": "",
"navbar": "",
"jumbotron": "",
"content": "",
"exception": "",
"exception_text": "",
"exception_title": "",
"loading": ""
};
// لایه رویدادها
var evts = {};
//لایه [dao]
var dao = {};
// ------------ آمادهسازی سند
$(document).ready(function () {
// ابتدای سند
console.log("document.ready");
// اجزای صفحه
ui.navbar = $("#navbar");
ui.jumbotron = $("#jumbotron");
ui.content = $("#content");
ui.erreur = $("#erreur");
ui.exception = $("#exception");
ui.exception_text = $("#exception-text");
ui.exception_title = $("#exception-title");
//صفحه ورود در حافظه پنهان ذخیره شده است تا قابل بازیابی باشد
ui.login.lang = ui.langue;
ui.login.navbar = ui.navbar.html();
ui.login.jumbotron = ui.jumbotron.html();
ui.login.content = ui.content.html();
// URL برای سرویس
$("#urlService").val(ui.urlService);
});
// ------------------------ توابع инициализация مؤلفههای Bootstrap
ui.initNavBarStart = function () {
...
};
ui.initNavBarRun = function () {
...
};
ui.initChoixMedecinJour = function () {
...
};
ui.updateCalendar = function (renew) {
...
};
// روز انتخابی را نمایش میدهد
ui.displayJour = function () {
...
};
ui.initAgenda = function () {
...
};
ui.initResa = function () {
...
};
- برای جداسازی لایهها از یکدیگر، تصمیم گرفته شد که آنها را در سه شیء قرار دهیم:
- [ui] برای لایه [présentation] (خطوط ۲–۲۷)،
- [evts] برای لایه مدیریت رویداد (خط ۲۹)،
- [dao] برای لایه [DAO] (خط ۳۱)؛
این تفکیک لایهها به سه شیء به جلوگیری از تعدادی تداخل بر سر نامهای متغیرها و توابع کمک میکند. هر لایه از متغیرها و توابعی استفاده میکند که با نام شیء محصورکننده آن لایه پیشوند شدهاند.
- خطوط ۳۸–۴۴: فیلدهایی که همیشه، صرفنظر از نماهای نمایش داده شده، وجود خواهند داشت، ذخیره میشوند. این کار از جستجوهای تکراری و غیرضروری برای jQuery جلوگیری میکند؛
- خطوط ۴۶–۴۹: صفحه بوت بهصورت محلی ذخیره میشود تا در صورتی که کاربر از سیستم خارج شود و زبان را تغییر نداده باشد، قابل بازیابی باشد؛
- خطوط ۵۴–۸۳: توابعی برای راهاندازی کامپوننتهای Bootstrap. تمام این موارد در بخش کامپوننتهای Bootstrap در پاراگراف ۸.۶.۴ پوشش داده شده است؛
8.6.8.4. توابع ابزار لایه [événements]
![]() |
روندپردازهای رویداد در فایل [evts.js] قرار داده شدهاند. چندین تابع بهطور منظم توسط این روندپردازها استفاده میشوند. اکنون آنها را معرفی میکنیم:
// شروع انتظار
evts.beginWaiting = function () {
// شروع انتظار
ui.loading = $("#loading");
ui.loading.show();
ui.exception.hide();
ui.erreur.hide();
evts.travailEnCours = true;
};
//پایان انتظار
evts.stopWaiting = function () {
//پایان انتظار
evts.travailEnCours = false;
ui.loading = $("#loading");
ui.loading.hide();
};
//نمایش نتیجه
evts.showResult = function (result) {
// نمایش دادههای دریافتی
var data = result.data;
// در حال تجزیه و تحلیل وضعیت
switch (result.status) {
case 1:
// خطا؟
if (data.status == 2) {
ui.erreur.html(data.content);
ui.erreur.show();
} else {
if (data.navbar) {
ui.navbar.html(data.navbar);
}
if (data.jumbotron) {
ui.jumbotron.html(data.jumbotron);
}
if (data.content) {
ui.content.html(data.content)
}
if (data.agenda) {
ui.agenda = $("#agenda");
ui.resa = $("#resa");
}
}
break;
case 2:
// نمایش خطا
evts.showException(data);
break;
}
};
// ------------ توابع متفرقه
evts.showException = function (data) {
//نمایش خطا
ui.exception.show();
ui.exception_text.html(data);
ui.exception_title.text(ui.exceptionTitle[ui.langue]);
};
- خط ۲: تابع [evts.beginwaiting] قبل از هر اقدام غیرهمزمان [DAO] فراخوانی میشود؛
- خطوط ۴–۵: تصویر متحرک بارگذاری نمایش داده میشود؛
- خطوط ۶–۷: ناحیه نمایش خطا و استثنا پنهان میشود (این دو یکسان نیستند)؛
- خط ۸: ذکر میشود که یک وظیفه ناهمزمان در حال اجرا است؛
- خط ۱۲: تابع [evts.stopwaiting] پس از بازگشت نتیجه یک عمل غیرهمزمان [DAO] فراخوانی میشود؛
- خط ۱۴: مشخص میشود که وظیفهٔ غیرهمزمان به پایان رسیده است؛
- خط ۱۵: آیکون متحرک انتظار پنهان شده است؛
- خط ۲۰: تابع [evts.showResult] نتیجه [result] یک عمل غیرهمزمان [DAO] را نمایش میدهد. نتیجه یک شیء JS با شکل زیر است: {'status':status,'data':data,'sendMeBack':sendMeBack}.
- خطوط ۴۷–۵۰: در صورتی که [result.status==2] استفاده میشود. این زمانی رخ میدهد که سرور [Web1] پاسخی با هدر خطای HTTP (مثلاً 403 Forbidden) ارسال میکند. در این حالت، [data] رشتهای است که توسط سرور برای نشان دادن خطا ارسال میشود؛
- خط ۲۵: جایی که یک پاسخ معتبر از سرور [Web1] دریافت شده است. در این صورت فیلد [data] حاوی پاسخ سرور است: {'status':status,'navbar':navbar,'jumbotron':jumbotron,'agenda':agenda,'content':content};
- خط ۲۷: حالتی که سرور [Web1] یک پاسخ خطا ارسال کرده است {'status':2,'navbar':null,'jumbotron':null,'agenda':null,'content':errors};
- خطوط ۲۸–۲۹: نمای [erreurs] نمایش داده میشود؛
- خطوط ۳۱–۳۳: نوار ناوبری ممکن است نمایش داده شود؛
- خطوط ۳۴–۳۶: جمبوتراون ممکن است نمایش داده شود؛
- خطوط ۳۷–۳۹: فیلد [data.content] ممکن است نمایش داده شود. بسته به شرایط، این نمایانگر یکی از نماهای [accueil, agenda] است؛
- خطوط ۴۰–۴۳: اگر تقویم مجدداً تولید شده باشد، ارجاعات خاصی به اجزای آن بازیابی میشوند تا هر بار که مورد نیاز باشند، لازم نباشد دوباره جستجو شوند؛
- خط ۵۴: تابع [evts.showException] برای نمایش متن استثنا که در پارامتر آن [data] قرار دارد، طراحی شده است؛
- خطوط ۵۷–۵۸: متن استثنا نمایش داده میشود؛
- خط ۵۸: عنوان استثنا به زبان فعلی بستگی دارد؛
فایل [evts.js] بیش از ۳۰۰ خط کد دارد که من به طور کامل روی آن توضیح نمیدهم. من صرفاً چند مثال را برای روشن کردن هدف این لایه انتخاب میکنم.
8.6.8.5. ورود کاربر

کاربر با استفاده از تابع زیر وارد میشود:
// ------------------------ اتصال
evts.connecter = function () {
// بازیابی مقادیری که باید ارسال شوند
var login = $("#login").val().trim();
var passwd = $("#passwd").val().trim();
//تنظیم URL سرور
ui.urlService = $("#urlService").val().trim();
dao.setUrlService(ui.urlService);
//پارامترهای درخواست
var post = {
"user": {
"login": login,
"passwd": passwd
},
"lang": ui.langue
};
var sendMeBack = {
"user": {
"login": login,
"passwd": passwd
},
"caller": evts.connecterDone
};
//ارسال درخواست
evts.execute([{
"name": "accueil-sans-agenda",
"post": post,
"sendMeBack": sendMeBack
}]);
};
- خطوط ۴–۵: نام کاربری و رمز عبور کاربر بازیابی میشوند؛
- خطوط ۷–۸: URL از سرویس [Web1] بازیابی میشود. این مقدار در هر دو لایه [ui] و [dao] ذخیره میشود؛
- خطوط ۱۰–۱۶: مقداری که باید ارسال شود: زبان فعلی و کاربری که در حال تلاش برای ورود است؛
- خطوط 17–23: شیء [sendMeBack] شیئی است که به تابع [DAO] پاس میشود، تابعی که قرار است فراخوانی شود و باید شیء را به تابع در خط 22 بازگرداند. در اینجا، شیء [sendMeBack] نمایانگر کاربری است که در حال تلاش برای ورود است؛
- خطوط ۲۵–۲۹: تابع [evts.execute] قادر به اجرای توالیای از عملیات ناهمزمان است. در اینجا، یک لیست شامل یک عملیات واحد ارسال میشود. فیلدهای این لیست به شرح زیر است:
- [name]: نام اقدام ناهمزمان که باید اجرا شود،
- [post]: مقداری که باید به سرور [Web1] ارسال شود،
- [sendMeBack]: مقداری که عمل غیرهمزمان باید با نتیجه خود بازگرداند؛
قبل از ورود به جزئیات تابع [evts.execute]، بیایید نگاهی به تابع [evts.connecterDone] در خط ۲۲ بیندازیم. این تابعی است که تابع غیرهمزمان فراخوانیشده [DAO] باید نتیجه خود را به آن بازگرداند:
evts.connecterDone = function (result) {
// نمایش نتیجه
evts.showResult(result);
// اتصال موفقیتآمیز است؟
if (result.status == 1 && result.data.status == 1) {
// صفحه
ui.page = "accueil-sans-agenda";
// کاربر وارد شده است
ui.user = result.sendMeBack.user;
}
};
- خط ۳: نتیجه بازگرداندهشده توسط سرور [Web1] نمایش داده میشود؛
- خط ۵: اگر این نتیجه حاوی هیچ خطایی نباشد، آنگاه ماهیت صفحه جدید (خط ۷) و کاربر احراز هویت شده (خط ۹) ذخیره میشوند؛
تابع [evts.execute] مجموعهای از عملیات غیرهمزمان را انجام میدهد:
// اجرای یک دنباله از اقدامات
evts.execute = function (actions) {
// کار در حال انجام؟
if (evts.travailEnCours) {
// در حال انجام هیچ کاری نیست
return;
}
// منتظر
evts.beginWaiting();
// اجرای اقدامات
dao.doActions(actions, evts.stopWaiting);
};
- خط ۲: پارامتر [actions] فهرستی از عملیات غیرهمزمان است که باید اجرا شوند؛
- خطوط ۴–۷: اجرای آن تنها در صورتی مجاز است که هیچ نمونه دیگری در حال اجرا نباشد؛
- خط ۹: انتظار آغاز میشود؛
- خط ۱۱: به لایه [DAO] دستور داده میشود تا توالی اقدامات را اجرا کند. پارامتر دوم نام تابع است که پس از بازگشت نتایج تمام اقدامات در توالی، اجرا میشود؛
در این مرحله وارد جزئیات تابع [dao.doActions] نمیشویم. اکنون به رویداد دیگری میپردازیم.
8.6.8.6. تغییر زبان

تغییر زبان توسط تابع زیر مدیریت میشود:
// ------------------------ تغییر زبان
evts.setLang = function (lang) {
// تغییر زبان؟
if (lang == ui.langue) {
// کاری انجام نده
return;
}
// زبان جدید
ui.langue = lang;
// کدام صفحه نیاز به ترجمه دارد؟
switch (ui.page) {
case "login":
evts.getLogin();
break;
case "accueil-sans-agenda":
evts.getAccueilSansAgenda();
break;
case "accueil-avec-agenda":
evts.getAccueilAvecAgenda(ui);
break;
}
};
- خط ۲: پارامتر [lang] زبان جدید است: 'fr' یا 'en';
- خطوط ۴–۷: اگر زبان جدید، زبان فعلی باشد، هیچ اقدامی انجام نمیشود؛
- خط ۹: زبان جدید ذخیره میشود؛
- خطوط ۱۲–۲۰: در صورت تغییر زبان، صفحهٔ در حال نمایش توسط مرورگر باید دوباره بارگذاری شود. سه صفحهٔ ممکن وجود دارد:
- صفحهای که [login] نامیده میشود، که در آن صفحه نمایش داده شده، صفحه احراز هویت است،
- صفحهای به نام [accueil-sans-agenda]، که صفحهای است که بلافاصله پس از احراز هویت موفق نمایش داده میشود،
- صفحه با نام [accueil-avec-agenda]، که به محض نمایش اولین تقویم، نمایش داده میشود. سپس تا زمانی که کاربر از سیستم خارج شود، روی صفحه باقی میماند؛
اکنون به صفحه [accueil-avec-agenda] میپردازیم. سه نسخه از این تابع وجود دارد:
![]() |
- نسخه [ getAccueilAvecAgenda-one] یک اقدام غیرهمزمان واحد را اجرا میکند؛
- نسخه [ getAccueilAvecAgenda-parallel] چهار عمل غیرهمزمان را بهصورت موازی اجرا میکند؛
- نسخه [ getAccueilAvecAgenda-sequence] چهار عمل غیرهمزمان را پشت سر هم اجرا میکند؛
8.6.8.7. تابع [ getAccueilAvecAgenda-one]
این تابع زیر است:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// پارامترهای درخواست
var post = {
"user": ui.user,
"lang": ui.langue,
"idMedecin": ui.idMedecin,
"jour": ui.jourAgenda
};
var sendMeBack = {
"caller": evts.getAccueilAvecAgendaDone
};
// درخواست
evts.execute([{
"name": "accueil-avec-agenda",
"post": post,
"sendMeBack": sendMeBack
}]);
};
- خطوط ۴–۹: مقداری که باید ارسال شود شامل کاربر واردشده، زبان موردنظر، شماره دکتر موردنظر برای دفترچه خاطرات و روز موردنظر برای یادداشت دفترچه خاطرات است؛
- خطوط ۱۰–۱۲: شیء [sendMeBack] شیئی است که در خط ۱۱ به تابع بازگردانده میشود. در اینجا، این شیء حاوی هیچ اطلاعاتی نیست؛
- خطوط 14–18: اجرای یک توالی از اقدامات ناهمزمان، یعنی [accueil-avec-agenda] (خط 15);
- خط ۱۱: تابعی که پس از بازگشت نتیجهٔ اقدام غیرهمزمان [accueil-avec-agenda] اجرا میشود؛
تابع [evts.getAccueilAvecAgendaDone] در خط ۱۱ نتیجه تابع غیرهمزمان به نام [accueil-avec-agenda] را نمایش میدهد:
evts.getAccueilAvecAgendaDone = function (result) {
// نمایش نتیجه
evts.showResult(result);
// صفحه جدید؟
if (result.status == 1 && result.data.status == 1) {
ui.page = "accueil-avec-agenda";
}
};
- خط ۱: [result] نتیجه تابع غیرهمزمان به نام [accueil-avec-agenda] است؛
- خط ۳: این نتیجه نمایش داده میشود؛
- خط ۵: اگر نتیجه بدون خطا باشد، صفحه جدید ثبت میشود (خط ۶)؛
8.6.8.8. تابع [ getAccueilAvecAgenda-parallel]
این تابع زیر است:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// اقدامات [navbar-run, jumbotron, accueil, agenda] in //
// navbar-run
var navbarRun = {
"name": "navbar-run"
};
navbarRun.post = {
"lang": ui.langue
};
navbarRun.sendMeBack = {
"caller": evts.showResult
};
// جَمبوترون
var jumbotron = {
"name": "jumbotron"
};
jumbotron.post = {
"lang": ui.langue
};
jumbotron.sendMeBack = {
"caller": evts.showResult
};
// خانه
var accueil = {
"name": "accueil"
};
accueil.post = {
"lang": ui.langue,
"user": ui.user
};
accueil.sendMeBack = {
"caller": evts.showResult
};
// تقویم
var agenda = {
"name": "agenda"
};
agenda.post = {
"user": ui.user,
"lang": ui.langue,
"idMedecin": ui.idMedecin,
"jour": ui.jourAgenda
};
agenda.sendMeBack = {
'idMedecin': ui.idMedecin,
'روز': ui.jourAgenda,
"caller": evts.getAgendaDone
};
// اجرای اقدامات در //
evts.execute([navbarRun, jumbotron, accueil, agenda])
};
- خط ۵۱: این بار، چهار عمل غیرهمزمان اجرا میشوند. آنها بهصورت موازی اجرا خواهند شد؛
- خطوط ۵–۱۳: تعریف عمل [navbarRun] که نوار ناوبری [navbar-run] را بازیابی میکند؛
- خط ۱۲: تابع مورد نظر پس از بازگشت نتیجه از اقدام غیرهمزمان [navbarRun] اجرا میشود؛
- خطوط ۱۵–۲۳: تعریف عمل [jumbotron] که نما [jumbotron] را بازیابی میکند؛
- خط ۲۲: تابع قابل اجرا زمانی که عمل غیرهمزمان [jumbotron] نتیجه خود را بازگردانده است؛
- خطوط ۲۵–۳۴: تعریف عمل [accueil]، که نما [accueil] را بازیابی میکند؛
- خط ۳۳: تابع قابل اجرا زمانی که عمل غیرهمزمان [accueil] نتیجه خود را بازگردانده است؛
- خطوط ۳۶–۴۹: تعریف عمل [agenda]، که نما [jumbotron] را بازیابی میکند؛
- خط ۴۸: تابع قابل اجرا زمانی که عمل غیرهمزمان [agenda] نتیجه خود را بازگردانده است؛
8.6.8.9. تابع [ getAccueilAvecAgenda-sequence]
این تابع زیر است:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// اقدامات [navbar-run, jumbotron, accueil, agenda] به ترتیب
// تقویم
var agenda = {
"name" : "agenda"
};
agenda.post = {
"user" : ui.user,
"lang" : ui.langue,
"idMedecin" : ui.idMedecin,
"jour" : ui.jourAgenda
};
agenda.sendMeBack = {
'idMedecin': ui.idMedecin,
'روز: ui.jourAgenda,
"caller" : evts.getAgendaDone
};
//خانه
var accueil = {
"name" : "accueil"
};
accueil.post = {
"lang" : ui.langue,
"user" : ui.user
};
accueil.sendMeBack = {
"caller" : evts.showResult,
"next" : agenda
};
// جَمبوترون
var jumbotron = {
"name" : "jumbotron"
};
jumbotron.post = {
"lang" : ui.langue
};
jumbotron.sendMeBack = {
"caller" : evts.showResult,
"next" : accueil
};
// navbar-run
var navbarRun = {
"name" : "navbar-run"
};
navbarRun.post = {
"lang" : ui.langue
};
navbarRun.sendMeBack = {
"caller" : evts.showResult,
"next" : jumbotron
};
// اجرای اقدامات به ترتیب
evts.execute([ navbarRun ])
};
- خط ۵۴: اقدام [navbarRun] اجرا میشود. پس از اتمام آن، برنامه به مورد بعدی میرود: [jumbotron]، خط ۵۱. این اقدام نیز به نوبه خود اجرا میشود. پس از اتمام آن، برنامه به مورد بعدی میرود: [accueil]، خط ۴۰. این مورد نیز به نوبه خود اجرا میشود. پس از اتمام آن، سیستم به مورد بعدی میرود: [agenda]، خط ۲۹. سپس این مورد نیز به ترتیب اجرا میشود. پس از اتمام آن، فرآیند متوقف میشود زیرا اقدام [agenda] اقدام بعدی ندارد.
8.6.8.10. لایه [DAO]
![]() |
فایل [dao.js] تمام توابع لایه [DAO] را گرد هم میآورد. ما این موارد را به ترتیب ارائه خواهیم داد:
// URL نمایانشده توسط سرور
dao.urls = {
"login": "/getLogin",
"accueil": "/getAccueil",
"jumbotron": "/getJumbotron",
"agenda": "/getAgenda",
"supprimerRv": "/supprimerRv",
"validerRv": "/validerRv",
"navbar-start": "/getNavbarStart",
"navbar-run": "/getNavbarRun",
"accueil-sans-agenda": "/getNavbarRunJumbotronAccueil",
"accueil-avec-agenda": "/getNavbarRunJumbotronAccueilAgenda"
};
// --------------- رابط
// URL سرور
dao.setUrlService = function (urlService) {
dao.urlService = urlService;
};
- خطوط ۱۶–۱۸: تابع که URL را برای سرویس [Web1] تنظیم میکند؛
- خطوط ۲–۱۳: فرهنگ لغت که نام یک عمل غیرهمزمان را به URL سرور [Web1] که باید پرسوجو شود، پیوند میدهد؛
// ------------------ مدیریت اقدام عمومی
// اجرای توالیای از عملیات غیرهمزمان
dao.doActions = function (actions, done) {
//پردازش اقدام
dao.actionsCount = actions.length;
dao.actionIndex = 0;
for (var i = 0; i < dao.actionsCount; i++) {
//درخواست ناهمزمان DAO
var deferred = $.Deferred();
deferred.done(dao.actionDone);
dao.doAction(deferred, actions[i], done);
}
};
- خط ۳: تابع [dao.doActions] یک توالی از عملیات غیرهمزمان [actions] را اجرا میکند. پارامتر [done] تابعی است که پس از بازگشت نتایج تمام عملیات اجرا میشود؛
- خطوط ۷–۱۲: عملیات غیرهمزمان بهصورت موازی اجرا میشوند. با این حال، اگر یکی از آنها دارای یک عملیات بعدی باشد، آن عملیات بعدی پس از اتمام عملیات قبلی اجرا میشود؛
- خط ۹: یک شیء [Deferred] در وضعیت [pending];
- خط ۱۰: هنگامی که این شیء به حالت [resolved] منتقل میشود، تابع [dao.actionDone] اجرا خواهد شد؛
- خط ۱۱: اقدام شماره i در لیست بهصورت غیرهمزمان اجرا میشود. پارامتر [done] از خط ۳ بهعنوان پارامتر ارسال میشود؛
تابع [dao.actionDone] که در پایان هر عمل غیرهمزمان اجرا میشود، به شرح زیر است:
//نتیجه دریافت شده است
dao.actionDone = function (result) {
//تماسگیرنده؟
var sendMeBack = result.sendMeBack;
if (sendMeBack && sendMeBack.caller) {
sendMeBack.caller(result);
}
//بعدی؟
if (sendMeBack && sendMeBack.next) {
//درخواست ناهمزمان DAO
var deferred = $.Deferred();
deferred.done(dao.actionDone);
dao.doAction(deferred, sendMeBack.next, sendMeBack.done);
}
//انجام شد؟
dao.actionIndex++;
if (dao.actionIndex == dao.actionsCount) {
// انجام شد؟
if (sendMeBack && sendMeBack.done) {
sendMeBack.done(result);
}
}
};
- خط ۲: تابع [dao.actionDone] نتیجه [result] را از یکی از عملیات غیرهمزمان در لیست عملیات قابل اجرا دریافت میکند؛
- خطوط ۴–۷: اگر عمل غیرهمزمان تکمیلشده، تابع مشخصی را برای بازگرداندن نتیجه تعیین کرده باشد، آن تابع فراخوانی میشود؛
- خطوط ۹–۱۴: اگر عمل غیرهمزمان تکمیلشده جانشینی داشته باشد، آن عمل نیز به نوبه خود اجرا میشود؛
- خط ۱۶: یک عمل تکمیل میشود. شمارشگر برای اعمال تکمیلشده افزایش مییابد. عملی که دارای تعداد نامعلومی عمل بعدی است، به عنوان یک عمل واحد محسوب میشود؛
- خطوط ۱۹–۲۱: اگر در ابتدا مشخص شده بود که تابع [done] پس از بازگشت نتایج تمام عملیاتهای بعدی یکبار اجرا شود، آنگاه این تابع اکنون اجرا میشود؛
متد [dao.doAction] یک عمل غیرهمزمان را اجرا میکند:
//اجرای یک عمل
dao.doAction = function (deferred, action, done) {
// تابع «done» باید در اقدام جاسازی شود
if (action.sendMeBack) {
action.sendMeBack.done = done;
} else {
action.sendMeBack = {
"done": done
};
}
//اجرای اقدام
dao.executePost(deferred, action.sendMeBack, dao.urls[action.name], action.post)
};
- خطوط ۴–۱۰: همانطور که همین حالا دیدیم، تابع مسئول پردازش نتیجه اقدام ناهمزمان که باید اجرا شود، باید به تابع [done] دسترسی داشته باشد. برای دستیابی به این هدف، مورد دوم را در شی [sendMeBack] قرار میدهیم که بخشی از نتیجه عملیات غیرهمزمان را تشکیل خواهد داد؛
- خط ۱۲: تابع [dao.executePost] اجرا میشود که فراخوانی HTTP را روی سرور [Web1] انجام میدهد. هدف URL، URL مرتبط با نام عملی است که باید اجرا شود؛
تابع [dao.executePost] فراخوانی HTTP را اجرا میکند:
//درخواست HTTP
dao.executePost = function (deferred, sendMeBack, url, post) {
// یک فراخوانی Ajax بهصورت دستی انجام میشود
$.ajax({
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
url: dao.urlService + url,
type: 'POST',
data: JSON3.stringify(post),
dataType: 'json',
success: function (data) {
// نتیجه بازگردانده میشود
deferred.resolve({
"status": 1,
"data": data,
"sendMeBack": sendMeBack
});
},
error: function (jqXHR, textStatus, errorThrown) {
var data;
if (jqXHR.responseText) {
data = jqXHR.responseText;
} else {
data = textStatus;
}
//خطا بازگردانده میشود
deferred.resolve({
"status": 2,
"data": data,
"sendMeBack": sendMeBack
});
}
});
};
ما قبلاً با این تابع مواجه شده و آن را مورد بحث قرار دادهایم. به سادگی ارزش دارد در خط ۹ توجه شود که هدف URL، الحاق URL از سرور [Web1] با URL مرتبط با نام اقدام است.
8.6.8.11. صفحهٔ بوت
![]() |

صفحه بوت [boot.html] نمای نشان داده شده در بالا را نمایش میدهد. این تنها صفحهای است که مستقیماً توسط مرورگر بارگذاری میشود. صفحات دیگر از طریق فراخوانیهای Ajax بازیابی میشوند. کد آن به شرح زیر است:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>RdvMedecins</title>
<!-- Bootstrap اصلی CSS -->
<link rel="stylesheet" href="css/bootstrap-3.1.1-min.css"/>
<link rel="stylesheet" type="text/css" href="css/bootstrap-select.min.css"/>
<link rel="stylesheet" type="text/css" href="css/datepicker3.css"/>
<link rel="stylesheet" type="text/css" href="css/footable.core.min.css"/>
<!-- استایلهای سفارشی برای این قالب -->
<link rel="stylesheet" type="text/css" href="css/rdvmedecins.css"/>
<!--هسته Bootstrap JavaScript ================================================== -->
<script type="text/javascript" src="vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="vendor/bootstrap.js"></script>
<script type="text/javascript" src="vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="vendor/bootstrap-datepicker.fr.js"></script>
<script type="text/javascript" src="vendor/footable.js"></script>
<!-- اسکریپتهای کاربر -->
<script type="text/javascript" src="js/json3.js"></script>
<script type="text/javascript" src="js/ui.js"></script>
<script type="text/javascript" src="js/evts.js"></script>
<script type="text/javascript" src="js/getAccueilAvecAgenda-sequence.js"></script>
<script type="text/javascript" src="js/dao.js"></script>
</head>
<body id="body">
<div id="navbar">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="images/loading.gif" alt="waiting..." style="display: none"/>
<!-- فرم ورود -->
<div class="navbar-form navbar-right" role="form" id="formulaire">
<div class="form-group">
<input type="text" placeholder="URL du serveur" class="form-control" id="urlService"/>
</div>
<div class="form-group">
<input type="text" placeholder="Utilisateur" class="form-control" id="login"/>
</div>
<div class="form-group">
<input type="password" placeholder="Mot de passe" class="form-control" id="passwd"/>
</div>
<button type="button" class="btn btn-success" onclick="javascript:evts.connecter()">Connexion</button>
<!-- زبانها -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langue</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span> <span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="javascript:evts.setLang('fr')">Français</a></li>
<li><a href="javascript:evts.setLang('en')">English</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container">
<!-- بوتاسترپ جامبوترون -->
<div id="jumbotron">
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="images/caduceus.jpg" alt="RvMedecins"/>
</div>
<div class="col-md-10">
<h1>
Cabinet médical<br/>Les Médecins associés
</h1>
</div>
</div>
</div>
</div>
<!-- پیامهای خطا -->
<div id="erreur"></div>
<div id="exception" class="alert alert-danger" style="display: none">
<h3 id="exception-title"></h3>
<span id="exception-text"></span>
</div>
<!--محتوا -->
<div id="content">
<div class="alert alert-info">Authentifiez-vous pour accéder à l'application</div>
</div>
</div>
<!-- ابتدای صفحه -->
<script>
// در حال راهاندازی صفحه
ui.langue = 'fr';
ui.exceptionTitle['fr'] = "L'erreur suivante s'est produite côté serveur :";
ui.exceptionTitle['en'] = "The following server error was met:";
ui.initNavBarStart();
</script>
</body>
</html>
- ما پیش از این با این نوع صفحه در فصل مربوط به بوتاسترپ (بخش 8.6.4) مواجه شدهایم؛
- خطوط ۹۹–۱۰۵: инициалиزاسيون برخی از عناصر لایه [présentation]؛
- خط ۲۷: از اسکریپت [getAccueilAvecAgenda-sequence.js] استفاده میشود. با تغییر اسکریپت در این خط، سه رفتار مختلف برای فراخوانی صفحه [accueil-avec-agenda] به دست میآوریم:
- [getAccueilAvecAgenda-one.js] صفحه را با یک فراخوانی واحد به HTTP بازیابی میکند،
- [getAccueilAvecAgenda-parallel.js] صفحه را با استفاده از چهار فراخوانی همزمان به HTTP بازیابی میکند،
- [getAccueilAvecAgenda-sequence.js] صفحه را با چهار فراخوانی متوالی به HTTP بازیابی میکند؛
8.6.8.12. Tests
روشهای مختلفی برای انجام آزمایشها وجود دارد. در اینجا، از ابزار [Webstorm] استفاده خواهیم کرد:
![]() |
- در [1]، یک پروژه را باز میکنیم. بهسادگی پوشه [2] را که حاوی ساختار درختی ایستا (HTML, CSS, JS) سایت مورد آزمایش است، انتخاب کنید؛
![]() |
- به [3]، سایت ایستا؛
- به [4-5]، صفحه [boot.html] بارگذاری میشود؛
![]() |
- در [5]، میتوانیم ببینیم که یک سرور تعبیهشده در [Webstorm] صفحه [boot.html] را از پورت [63342] ارائه کرده است. این یک نکته مهم است که باید درک شود، زیرا به این معنی است که اسکریپتهای موجود در صفحه [boot.html] درخواستهای بیندامنهای به سرور [Web1] ارسال خواهند کرد، که به نوبه خود روی [localhost:8081] در حال اجرا است. مرحلهای که [boot.html] را بارگذاری کرده است، میداند که آن را از [localhost:63342] بارگذاری کرده است. بنابراین به این صفحه اجازه نمیدهد تا به سایت [localhost:8081] فراخوانی انجام دهد، زیرا پورت یکسان نیست. بنابراین، درخواستهای بیندامنهای توصیفشده در بخش 8.4.14 را پیادهسازی خواهد کرد. به همین دلیل، برنامه [Web1] باید طوری پیکربندی شود که این درخواستهای بیندامنهای را بپذیرد. این موضوع در فایل [AppConfig] روی سرور Spring/Thymeleaf تعیین میشود:
![]() |
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {
// مدیر / مدیر
private final String USER_INIT = "admin";
private final String MDP_USER_INIT = "admin";
// ریشه سرویس وب / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// زمانانتظار به میلیثانیه
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
...
ما آزمایش کلاینت JS را به خواننده واگذار میکنیم. این کلاینت باید قادر به بازتولید عملکرد توصیفشده در بخش 8.6.3 باشد.
پس از تأیید صحت کلاینت JS، میتوان آن را در دایرکتوری سرور [Web1] مستقر کرد تا از احتیاج به تأیید درخواستهای بیندامنهای جلوگیری شود:
![]() |
در بالا، ما سایت آزمایششده را در پوشه [src / main / resources / static] کپی کردهایم. سپس میتوانیم URL و [http://localhost:8081/boot.html] را درخواست کنیم:

اکنون دیگر به درخواستهای بیندامنهای نیازی نداریم و میتوانیم موارد زیر را در فایل پیکربندی [AppConfig] روی سرور [Web1] بنویسیم:
// CORS
private final boolean CORS_ALLOWED=false;
برنامه بالا همچنان کار خواهد کرد. با این حال، اگر به برنامه [Webstorm] بازگردیم، دیگر کار نخواهد کرد:


اگر به کنسول توسعهدهنده (Ctrl-Shift-I) بروید، علت خطا را مشاهده خواهید کرد:

این یک خطای درخواست بیندامنهای غیرمجاز است.
8.6.8.13. Conclusion
ما معماری زیر را پیادهسازی کردهایم: JS
![]() |
- لایهها کاملاً از هم جدا شدهاند؛
- ما یک برنامه از نوع APU (برنامه تکصفحهای) داریم. این ویژگی اکنون به ما امکان میدهد تا یک برنامه بومی برای دستگاههای مختلف موبایل (اندروید، IoS، ویندوز فون) تولید کنیم؛
- ما مدلی ایجاد کردهایم که قادر به اجرای عملیات غیرهمزمان به صورت موازی، متوالی، یا ترکیبی از هر دو است؛
8.6.9. مرحله ۶: تولید یک اپلیکیشن بومی برای اندروید
ابزار [Phonegap] [http://phonegap.com/] تولید یک فایل اجرایی موبایل (اندروید، IoS، ویندوز ۸، ...) از یک برنامه HTML / JS / CSS. راههای مختلفی برای دستیابی به این امر وجود دارد. ما از سادهترین روش استفاده خواهیم کرد: یک ابزار آنلاین موجود در وبسایت PhoneGap به آدرس [http://build.phonegap.com/apps]. این ابزار فایل ZIP حاوی وبسایت ایستا را که باید تبدیل شود، «بارگذاری» خواهد کرد. صفحه بوت باید با نام [index.html] نامگذاری شود. بنابراین ما صفحه [boot.html] را به [index.html] تغییر نام میدهیم:
![]() |
سپس پوشه را زیپ میکنیم، در این مورد [rdvmedecins-client-js-03]. در ادامه به وبسایت PhoneGap به آدرس [http://build.phonegap.com/apps] میرویم:
![]() |
- قبل از [1]، ممکن است لازم باشد یک حساب کاربری ایجاد کنید؛
- در [1]، شروع میکنیم؛
- در [2]، یک طرح رایگان را انتخاب میکنیم که فقط یک اپلیکیشن PhoneGap را مجاز میداند؛
![]() |
- در [3]، اپلیکیشن فشردهشده [4] را دانلود کنید؛
![]() |
- در [5]، برای برنامه یک نام انتخاب کنید؛
- در [6]، آن را بسازید. این کار ممکن است تا ۱ دقیقه طول بکشد. لطفاً منتظر بمانید تا آیکونهای پلتفرمهای مختلف موبایل نشان دهند که ساخت کامل شده است؛
![]() |
- فقط باینری اندروید [7] و باینری ویندوز [8] تولید شدهاند؛
- برای دانلود باینری اندروید روی [7] کلیک کنید؛
![]() |
- در [9]، باینری دانلود شده [apk]؛
یک شبیهساز [GenyMotion] برای یک تبلت اندروید را راهاندازی کنید (به بخش 9.9 مراجعه کنید):
![]() |
در بالا، ما یک شبیهساز تبلت با اندروید نسخه API 19 را راهاندازی میکنیم. پس از راهاندازی شبیهساز،
- با کشیدن اسلایدر (در صورت وجود) به کنار و سپس رها کردن آن، آن را باز کنید؛
- با استفاده از ماوس، فایل [PGBuildApp-debug.apk] را که دانلود کردهاید بکشید و در روی شبیهساز رها کنید. سپس نصب و اجرا خواهد شد؛
![]() |
شما باید URL را به [1] تغییر دهید. برای این کار، در یک پنجرهٔ فرمان، دستور [ipconfig] (خط ۱ زیر) را تایپ کنید که آدرسهای مختلف IP را روی دستگاه شما نمایش خواهد داد:
C:\Users\Serge Tahé>ipconfig
Configuration IP de Windows
Carte réseau sans fil Connexion au réseau local* 15 :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
Carte Ethernet Connexion au réseau local :
Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
Adresse IPv6 de liaison locale. . . . .: fe80::698b:455a:925:6b13%4
Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
Masque de sous-réseau. . . . . . . . . : 255.255.0.0
Passerelle par défaut. . . . . . . . . : 172.19.0.254
Carte réseau sans fil Wi-Fi :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
...
آدرس وایفای IP (خطوط ۶–۹) یا آدرس شبکه محلی IP (خطوط ۱۱–۱۷) را یادداشت کنید. سپس از این آدرس IP در بخش URL وبسرور استفاده کنید:
![]() |
پس از انجام این کار، به سرویس وب وارد شوید:
![]() |
برنامه را روی شبیهساز تست کنید. باید کار کند. در سمت سرور، ممکن است بخواهید سربرگهای CORS را در کلاس [ApplicationModel] مجاز کنید یا نکنید:
// CORS
private final boolean CORS_ALLOWED=false;
این موضوع برای اپلیکیشن اندروید اهمیتی ندارد. این اپلیکیشن در مرورگر اجرا نمیشود. با این حال، نیاز به هدرهای CORS از سمت مرورگر است، نه سرور.
8.6.10. نتیجهگیری مطالعه موردی
ما معماری زیر را توسعه دادهایم:
![]() |
این یک معماری سهلایه پیچیده است. این معماری برای استفاده مجدد از لایه [Web2]، که لایه سرور برنامه [AngularJS-Spring MVC] در سند [Tutoriel AngularJS / Spring 4] بود، برای ... طراحی شده است.URL و [http://tahe.developpez.com/angularjs-spring4/]. صرفاً به همین دلیل است که ما یک معماری سهلایه داریم. در حالی که در برنامه [AngularJS-Spring MVC]، کلاینت برای [Web2] یک کلاینت [AngularJS] بود، در اینجا کلاینت برای [Web2] یک معماری دو لایه متشکل از [jQuery] و [Spring MVC / Thymeleaf] است. ما لایههایی اضافه کردهایم، بنابراین با کاهش عملکرد مواجه خواهیم شد.
برنامهای که در اینجا بررسی شده است، در طول زمان در سه سند مختلف توسعه یافته است:
- [Introduction aux frameworks JSF2, Primefaces et Primefaces mobile]، سپس URL و [http://tahe.developpez.com/java/primefaces/]. مطالعه موردی در ابتدا با استفاده از فریمورکهای JSF2 / PrimeFaces توسعه داده شد. PrimeFaces کتابخانهای از کامپوننتهای مبتنی بر AJAX است که نیاز به نوشتن جاوااسکریپت را از بین میبرد. اپلیکیشن توسعهیافته در آن زمان، کمتر از نمونه مورد بررسی در اینجا پیچیده بود. این اپلیکیشن یک نسخه وب استاندارد برای کامپیوترها و یک نسخه موبایل برای تلفنها داشت؛
- [Tutoriel AngularJS / Spring 4] تا URL [http://tahe.developpez.com/angularjs-spring4/]. برنامهای که در آن زمان توسعه یافته بود، ویژگیهای مشابهی با مورد بررسی در این سند داشت. این برنامه همچنین به اندروید منتقل شده بود؛
- این سند؛
از این کار، نکات زیر برای من برجسته است:
- اپلیکیشن [Primefaces] تا کنون سادهترین برای نوشتن بود و نسخه وب موبایلی آن بسیار کارآمد از آب درآمد. این برنامه به هیچ دانش جاوااسکریپتی نیاز ندارد. امکان بومیسازی آن برای پلتفرمهای OS دستگاههای موبایل مختلف وجود ندارد، اما آیا این کار ضروری است؟ به نظر میرسد تغییر سبک برنامه دشوار است. در واقع، ما با صفحهآراییهای سبک PrimeFaces کار میکنیم. این ممکن است یک نقطهضعف باشد؛
- نوشتن اپلیکیشن [AngularJS-Spring MVC] پیچیده بود. من دریافتم که چارچوب [AngularJS] وقتی که بخواهید در آن تسلط پیدا کنید، کاملاً دشوار است. معماری [client Angular] / [service web / jSON implémenté par Spring MVC] بهویژه تمیز و کارآمد است. این معماری را میتوان برای هر برنامه وب دیگری تکثیر کرد. این معماری است که به نظر من امیدوارکنندهترین است، زیرا از مهارتهای مختلفی هم در سمت کلاینت و هم در سمت سرور بهره میبرد. (JS+HTML+CSS در سمت کلاینت، جاوا یا زبان دیگری در سمت سرور)، که امکان توسعه همزمان کلاینت و سرور را فراهم میکند؛
- برای برنامهای که در این سند با استفاده از معماری سهلایه [client jQuery] / [serveur Web1 / Spring MVC / Thymeleaf] / [serveur Web2 / Spring MVC]، برخی ممکن است فناوری [jQuery+Spring MVC+Thymelaf] را نسبت به فناوری [AngularJS] آسانتر درک کنند. لایه [DAO] از کلاینت جاوااسکریپتی که نوشتهایم را میتوان در سایر برنامهها مجدداً استفاده کرد؛

























































































































































































































































































