2. سرور Spring 4
![]() |
در معماری فوق، اکنون به ساخت سرویس وب /JSON با استفاده از فریمورک Spring 4 میپردازیم. ما آن را در چندین مرحله خواهیم نوشت:
- ابتدا، لایههای [métier] و [DAO] (Data Access Object). در اینجا از Spring Data استفاده خواهیم کرد؛
- سپس سرویس وب JSON بدون احراز هویت. در اینجا از Spring MVC استفاده خواهیم کرد؛
- سپس با استفاده از Spring Security، مؤلفه احراز هویت را اضافه خواهیم کرد.
ما با توضیح ساختار پایگاه دادهٔ زیربنایی برنامه شروع میکنیم.
2.1. پایگاه داده
![]() |
پایگاه داده، که از این پس با نام [dbrdvmedecins] شناخته میشود، یک پایگاه داده MySQL5 است که شامل جداول زیر میباشد:
![]() |
قرارها توسط جداول زیر مدیریت میشوند:
- [medecins]: شامل فهرست پزشکان مطب است؛
- [clients]: شامل فهرست بیماران مطب است؛
- [creneaux]: شامل اسلاتهای قرار ملاقات برای هر پزشک است؛
- [rv]: شامل فهرست قرارهای ملاقات پزشکان است.
جدولهای [roles]، [users] و [users_roles] مربوط به احراز هویت هستند. در حال حاضر با اینها سروکار نخواهیم داشت.
روابط بین جداول مدیریت قرار ملاقاتها به شرح زیر است:
![]() |
- یک بازه زمانی متعلق به یک پزشک است – یک پزشک ۰ یا چند بازه زمانی دارد؛
- یک قرار ملاقات، مشتری و پزشک را از طریق یکی از بازههای زمانی پزشک گرد هم میآورد؛
- یک مراجعهکننده ۰ یا چند قرار ملاقات دارد؛
- یک بازه زمانی با ۰ یا چند قرار ملاقات (در روزهای مختلف) مرتبط است.
2.1.1. جدول [MEDECINS]
این جدول شامل اطلاعاتی دربارهٔ پزشکان مدیریتشده توسط برنامهٔ [RdvMedecins] است.
![]() | ![]() |
- ID: شماره شناسایی پزشک – کلید اصلی جدول
- VERSION: شمارهای که نسخهٔ سطر در جدول را شناسایی میکند. این شماره هر بار که تغییری در سطر ایجاد میشود، یک واحد افزایش مییابد.
- NOM: نام خانوادگی پزشک
- PRENOM: نام کوچک آنها
- TITRE: عنوان آنها (خانم، بانو، آقا)
2.1.2. جدول [CLIENTS]
بیماران پزشکان مختلف در جدول [CLIENTS] ثبت میشوند:
![]() | ![]() |
- ID: شماره شناسه مشتری – کلید اصلی جدول
- VERSION: شمارهای که نسخهٔ سطر در جدول را شناسایی میکند. این شماره هر بار که تغییری در سطر ایجاد میشود، یک واحد افزایش مییابد.
- NOM: نام خانوادگی مشتری
- PRENOM: نام کوچک آنها
- TITRE: عنوان آنها (خانم، بانو، آقای)
2.1.3. جدول [CRENEAUX]
این جدول بازههای زمانی را که ثبتنام در RV امکانپذیر است، فهرست میکند:
![]() |
![]() |
- ID: شمارهای که جایگاه زمانی را شناسایی میکند – کلید اصلی جدول (ردیف 8)
- VERSION: شمارهای که نسخهٔ سطر در جدول را شناسایی میکند. این شماره هر بار که تغییری در سطر اعمال میشود، یک واحد افزایش مییابد.
- ID_MEDECIN: شمارهای که پزشک مربوط به این اسلات را شناسایی میکند – کلید خارجی روی ستون MEDECINS (ID).
- HDEBUT: زمان شروع اسلات
- MDEBUT: دقیقه شروع اسلات
- HFIN: زمان پایان اسلات
- MFIN: دقایق پایان اسلات
رد دوم جدول [CRENEAUX] (به [1] بالا مراجعه کنید) نشان میدهد که اسلات شمارهٔ ۲ از ساعت ۸:۲۰ شروع و در ساعت ۸:۴۰ پایان مییابد و به پزشک شمارهٔ ۱ اختصاص داده شده است. (خانم ماری PELISSIER).
2.1.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 است.
2.2. مقدمهای بر Spring Data
ما قصد داریم لایه [DAO] پروژه را با استفاده از Spring Data، شاخهای از اکوسیستم Spring، پیادهسازی کنیم.
![]() |
وبسایت Spring دارای آموزشهای متعددی است که به شما در شروع کار با Spring [http://spring.io/guides] کمک میکند. ما از یکی از این آموزشها برای معرفی Spring Data استفاده خواهیم کرد. برای این کار، از Spring Tool Suite (STS) استفاده خواهیم کرد.
![]() |
- در [1]، ما یکی از آموزشهای [spring.io/guides] را وارد میکنیم؛
![]() |
- در [2]، ما آموزش [Accessing Data Jpa] را انتخاب میکنیم که نشان میدهد چگونه با استفاده از Spring Data به یک پایگاه داده دسترسی پیدا کنیم؛
- در [3]، ما پروژهای را که توسط Maven پیکربندی شده است، انتخاب میکنیم؛
- در [4]، آموزش در دو قالب موجود است: [initial]، که نسخهای خالی است و با دنبال کردن آموزش تکمیل میشود، یا [complete]، که نسخه نهایی آموزش است. ما دومی را انتخاب میکنیم؛
- در [5]، میتوانید انتخاب کنید که آموزش را در یک مرورگر مشاهده کنید؛
- در [6]، پروژه نهایی.
2.2.1. پیکربندی Maven پروژه
وابستگیهای Maven پروژه در فایل [pom.xml] پیکربندی شدهاند:
<groupId>org.springframework</groupId>
<artifactId>gs-accessing-data-jpa</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
<properties>
<!--برای همه چیز از UTF-8 استفاده کنید -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>hello.Application</start-class>
</properties>
- خطوط ۵–۹: تعریف یک پروژه 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 را مشخص میکند.
2.2.2. لایه [JPA]
دسترسی به پایگاه داده از طریق لایه [JPA]، Java Persistence API:
![]() |
![]() |
این برنامه ساده است و مشتریان [Customer] را مدیریت میکند. کلاس [Customer] بخشی از لایه [JPA] را تشکیل میدهد و به شرح زیر است:
package hello;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String firstName;
private String lastName;
protected Customer() {
}
public Customer(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
}
}
یک مشتری دارای شناسه [id]، نام [firstName] و نام خانوادگی [lastName] است. هر نمونه [Customer] نمایانگر یک سطر در یک جدول پایگاه داده است.
- خط ۸: anotation JPA، که به این معنی است که پایداری نمونههای [Customer] (ایجاد، خواندن، بهروزرسانی، حذف) توسط پیادهسازی JPA مدیریت خواهد شد. طبق وابستگیهای Maven، میتوان دید که پیادهسازی JPA / Hibernate در حال استفاده است؛
- خطوط ۱۱–۱۲: حاشیهنویسیهای JPA که فیلد [id] را با کلید اصلی جدول [Customer] مرتبط میکنند. خط ۱۲ نشان میدهد که پیادهسازی JPA از روش تولید کلید اصلی خاص SGBD مورد استفاده، در این مورد H2، استفاده خواهد کرد؛
هیچ یادداشت دیگری برای JPA وجود ندارد. بنابراین از مقادیر پیشفرض استفاده خواهد شد:
- جدول [Customer] به نام کلاس نامگذاری خواهد شد، یعنی [Customer]؛
- ستونهای این جدول بر اساس فیلدهای کلاس نامگذاری خواهند شد: [id, firstName, lastName]، با در نظر گرفتن اینکه در نامهای ستون جدول به حروف بزرگ و کوچک توجه نمیشود؛
توجه داشته باشید که پیادهسازی JPA مورد استفاده هرگز نامگذاری نمیشود.
2.2.3. لایه [DAO]
![]() |
![]() |
کلاس [CustomerRepository] لایه [DAO] را پیادهسازی میکند. کد آن به شرح زیر است:
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}
بنابراین این یک رابط است و نه یک کلاس (خط ۷). این رابط، رابط [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بستگی دارد: اگر**null**باشد، عملیات ماندگاری انجام میشود؛ در غیر این صورت، عملیات بهروزرسانی انجام میشود؛ - خط ۱۰: همین امر صدق میکند، اما برای یک لیست از اِنتِیتیها؛
- خط ۱۲: متد findOne برای بازیابی یک موجودیت T که با کلید اصلی
idشناسایی شده است، استفاده میشود؛ - خط ۲۲: متد delete برای حذف یک موجودیت 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] را با استفاده از یک رابط ساده پیادهسازی کنیم.
2.2.4. لایه [console]
![]() |
![]() |
کلاس [Application] به شرح زیر است:
package hello;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
//چند مشتری را ذخیره میکند
repository.save(new Customer("Jack", "Bauer"));
repository.save(new Customer("Chloe", "O'Brian"));
repository.save(new Customer("Kim", "Bauer"));
repository.save(new Customer("David", "Palmer"));
repository.save(new Customer("Michelle", "Dessler"));
//تمام مشتریان را بازیابی میکند
Iterable<Customer> customers = repository.findAll();
System.out.println("Customers found with findAll():");
System.out.println("-------------------------------");
for (Customer customer : customers) {
System.out.println(customer);
}
System.out.println();
// یک مشتری منفرد را با ID بازیابی میکند
Customer customer = repository.findOne(1L);
System.out.println("Customer found with findOne(1L):");
System.out.println("--------------------------------");
System.out.println(customer);
System.out.println();
//بازیابی مشتریان بر اساس نام خانوادگی
List<Customer> bauers = repository.findByLastName("Bauer");
System.out.println("Customer found with findByLastName('Bauer'):");
System.out.println("--------------------------------------------");
for (Customer bauer : bauers) {
System.out.println(bauer);
}
context.close();
}
}
- خط ۱۰: نشان میدهد که این کلاس برای پیکربندی Spring استفاده میشود. نسخههای اخیر Spring در واقع میتوانند به جای XML، به صورت جاوا پیکربندی شوند. هر دو روش میتوانند به طور همزمان استفاده شوند. در کد یک کلاس که دارای انوتیشن [Configuration] است، معمولاً بیینهای اسپرینگ، یعنی تعریف کلاسهایی که باید نمونهسازی شوند، یافت میشود. در اینجا، هیچ بیینی تعریف نشده است. باید توجه داشت که هنگام کار با یک SGBD، بیینهای اسپرینگ مختلفی باید تعریف شوند:
- یک [EntityManagerFactory] که پیادهسازی JPA مورد استفاده را تعریف میکند،
- یک [DataSource] که منبع دادهی مورد استفاده را تعریف میکند،
- یک بیون [TransactionManager] که مدیر تراکنش مورد استفاده را تعریف میکند؛
در اینجا، هیچیک از این بینها تعریف نشدهاند.
- خط ۱۱: anotation [EnableAutoConfiguration] یک anotation از پروژه [Spring Boot] (خطوط ۵–۶) است. این anotation به Spring Boot، از طریق کلاس [SpringApplication] (خط 16)، دستور میدهد که برنامه را بر اساس کتابخانههای موجود در classpath خود پیکربندی کند. از آنجا که کتابخانههای Hibernate در classpath قرار دارند، bean با شناسه [entityManagerFactory] با استفاده از Hibernate پیادهسازی خواهد شد. از آنجایی که کتابخانه SGBD در مسیر کلاس (classpath) قرار دارد، بین H2 با استفاده از [dataSource] پیادهسازی خواهد شد. در بیون [dataSource]، نام کاربری و رمز عبور نیز باید تعریف شوند. در اینجا، Spring Boot از مدیر پیشفرض در H2 استفاده خواهد کرد که رمز عبور ندارد. از آنجا که کتابخانه [spring-tx] در مسیر کلاسها قرار دارد، از مدیر تراکنش Spring استفاده خواهد شد.
علاوه بر این، پوشهای که حاوی کلاس [Application] است، برای یافتن بیونهایی که بهطور ضمنی توسط Spring شناسایی میشوند یا بهطور صریح توسط anotationهای Spring تعریف شدهاند، اسکن خواهد شد. بنابراین، کلاسهای [Customer] و [CustomerRepository] بررسی خواهند شد. از آنجا که کلاس اول دارای anotation [@Entity] است، بهعنوان یک انتیت که توسط Hibernate مدیریت میشود، فهرستبندی خواهد شد. از آنجا که کلاس دوم interface [CrudRepository] را extends میکند، بهعنوان یک Spring bean ثبت خواهد شد.
بیایید خطوط ۱۶–۱۷ کد را بررسی کنیم:
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
- خط ۱: متد استاتیک [run] از کلاس [SpringApplication] در پروژه Spring Boot اجرا میشود. پارامتر آن کلاسی است که دارای انوتیشن [Configuration] یا [EnableAutoConfiguration] است. سپس همه چیزهایی که قبلاً توضیح داده شد، اتفاق میافتد. نتیجه یک کانکست اپلیکیشن اسپرینگ (Spring application context) است، یعنی مجموعهای از بینها (beans) که توسط اسپرینگ مدیریت میشوند؛
- خط 17: ما یک بین (bean) را که رابط [CustomerRepository] را پیادهسازی میکند، از این کانکست Spring درخواست میکنیم. در اینجا، ما کلاسی را که توسط Spring Data برای پیادهسازی این رابط تولید شده است، بازیابی میکنیم.
عملیات بعدی به سادگی از متدهای بیانی که رابط [CustomerRepository] را پیادهسازی میکند، استفاده میکنند. توجه کنید که در خط ۵۰، کانکست بسته میشود. خروجی کنسول به شرح زیر است:
- خطوط ۱–۸: لوگوی پروژه Spring Boot؛
- خط ۹: کلاس [hello.Application] اجرا میشود؛
- خط ۱۰: [AnnotationConfigApplicationContext] کلاسی است که رابط [ApplicationContext] را در Spring پیادهسازی میکند. این یک کانتینر بین (bean) است؛
- خط ۱۱: بین [entityManagerFactory] با استفاده از کلاس [LocalContainerEntityManagerFactory]، یک کلاس Spring، پیادهسازی شده است؛
- خط ۱۲: [hibernate] ظاهر میشود. این پیادهسازی، JPA، است که انتخاب شده است؛
- خط ۱۹: یک گویش Hibernate، به طور خاص نوع SQL، قرار است با SGBD استفاده شود. در اینجا، گویش [H2Dialect] نشان میدهد که Hibernate با SGBD و H2 کار خواهد کرد؛
- خطوط 22–24: جدول [CUSTOMER] ایجاد میشود. این بدان معناست که Hibernate برای تولید جدولها از تعاریف JPA پیکربندی شده است؛ در این مورد، تعریف JPA از کلاس [Customer]؛
- خطوط ۲۷–۳۲: لاگهای Hibernate که درج ردیفها در جدول [CUSTOMER] را نشان میدهند. این بدان معناست که Hibernate برای تولید لاگها پیکربندی شده است؛
- خطوط ۳۵–۳۹: پنج مشتری درجشده؛
- خطوط ۴۲–۴۴: نتیجه متد [findOne] اینفریس؛
- خطوط ۴۷–۵۰: نتایج متد [findByLastName]؛
- از خط ۵۱ به بعد: لاگهای مربوط به بستهشدن کانکست Spring.
2.2.5. پیکربندی دستی پروژه Spring Data
ما پروژه قبلی را در پروژه [gs-accessing-data-jpa-2] کپی میکنیم:
![]() |
در این پروژه جدید، ما به پیکربندی خودکار ارائهشده توسط Spring Boot متکی نخواهیم بود. ما آن را بهصورت دستی پیکربندی خواهیم کرد. این کار در صورتی میتواند مفید باشد که پیکربندیهای پیشفرض با نیازهای ما مطابقت نداشته باشند.
ابتدا، وابستگیهای لازم را در فایل [pom.xml] مشخص میکنیم:
<dependencies>
<!-- Spring Core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<!--تراکنشهای Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<!--Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.5.2.RELEASE</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>1.0.2.RELEASE</version>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.3.4.Final</version>
</dependency>
<!-- H2 پایگاه داده -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.178</version>
</dependency>
<!-- 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>
- خطوط ۳–۱۷: کتابخانههای اصلی Spring؛
- خطوط ۱۹–۲۸: کتابخانههای Spring برای مدیریت تراکنشهای پایگاه داده؛
- خطوط ۳۰–۳۴: Spring Data، مورد استفاده برای دسترسی به پایگاه داده؛
- خطوط ۳۶–۴۰: Spring Boot برای راهاندازی برنامه؛
- خطوط ۴۸–۵۲: SGBD H2;
- خطوط 54–63: پایگاههای داده اغلب با استخرهای اتصال باز استفاده میشوند که از باز و بسته کردن مکرر اتصالات جلوگیری میکند. در اینجا، پیادهسازی مورد استفاده متعلق به [commons-dbcp] است؛
هنوز در [pom.xml]، نام کلاس اجرایی تغییر میکند:
<properties>
...
<start-class>demo.console.Main</start-class>
</properties>
در پروژه جدید، کلاس [Customer] و رابط [CustomerRepository] بدون تغییر باقی میمانند. ما کلاس [Application] را که به دو کلاس تقسیم خواهد شد، اصلاح خواهیم کرد:
- [Config]، که کلاس پیکربندی خواهد بود:
- [Main]، که کلاس اجرایی خواهد بود؛
![]() |
کلاس اجرایی [Main] مانند قبل است، بدون حاشیهنویسیهای پیکربندی:
package demo.console;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
public class Main {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Config.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
...
context.close();
}
}
- خط ۱۲: کلاس [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 تعیین میکند؛
- خط ۲۱: آناوتیشن [@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 وجود ندارد.
اجرای کد نتایج مشابهی با قبل تولید میکند.
2.2.6. ایجاد یک آرشیو قابل اجرا
برای ایجاد یک آرشیو اجرایی از پروژه، مراحل زیر را دنبال کنید:
![]() |
- در [1]: یک پیکربندی اجرای ایجاد میشود؛
- در [2]: از نوع [Java Application]
- در [3]: پروژهٔ اجرایی را مشخص میکند (از دکمهٔ Browse استفاده کنید);
- در [4]: کلاس قابل اجرا را مشخص میکند؛
- در [5]: نام پیکربندی اجرا – میتواند هر چیزی باشد؛
![]() |
- در [6]: پروژه صادر میشود؛
- در [7]: به شکل یک آرشیو اجرایی JAR؛
- در [8]: مسیر و نام فایل اجرایی که باید ایجاد شود را مشخص میکند؛
- در [9]: نام پیکربندی اجرای ایجاد شده در [5];
پس از انجام این کار، یک کنسول را در پوشهای که آرشیو اجرایی در آن قرار دارد، باز کنید:
آرشیو به شرح زیر اجرا میشود:
.....\dist>java -jar gs-accessing-data-jpa-2.jar
خروجیهای نمایشدادهشده در کنسول به شرح زیر است:
2.2.7. ایجاد یک پروژه جدید Spring Data
برای ایجاد یک قالب پروژه Spring Data، مراحل زیر را دنبال کنید:
![]() |
- در [1]، یک پروژه جدید ایجاد کنید؛
- در [2]: از نوع [Spring Starter Project];
- پروژه تولیدشده یک پروژه Maven خواهد بود. در [3]، نام گروه پروژه را مشخص کنید؛
- در [4]: نام محصول (در این مورد یک JAR) را مشخص کنید که هنگام ساخت پروژه ایجاد خواهد شد؛
- در [5]: بستهٔ کلاس قابل اجرا که در پروژه ایجاد میشود مشخص میگردد؛
- در [6]: نام اِکلیپس پروژه – این میتواند هر چیزی باشد (لازم نیست که با [4] یکسان باشد)؛
- در [7]: این مشخص میکند که یک پروژه با یک لایه [JPA] باید ایجاد شود. وابستگیهای مورد نیاز برای چنین پروژهای سپس در فایل [pom.xml] گنجانده خواهند شد؛
![]() |
- در [8]: پروژه ایجاد شده است؛
فایل [pom.xml] وابستگیهای مورد نیاز برای پروژه JPA را در بر میگیرد:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.0.RELEASE</version>
<relativePath/> <!--والد را از مخزن جستجو میکند -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- خطوط ۹–۱۲: وابستگیهای مورد نیاز برای 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] یک آنوتیشن JUnit (خط ۴) است، در حالی که کلاس [SpringJUnit4ClassRunner] یک کلاس Spring (خط ۶) است؛
اکنون که اسکلت برنامه JPA را داریم، میتوانیم آن را تکمیل کنیم تا لایه پایداری سرور برای برنامه مدیریت قرار ملاقاتهایمان را بنویسیم.
2.3. پروژه سرور اکلیپس
![]() |
![]() |
اجزای اصلی پروژه به شرح زیر هستند:
- [pom.xml]: فایل پیکربندی Maven پروژه؛
- [rdvmedecins.entities]: انتیتهای JPA؛
- [rdvmedecins.repositories]: رابطهای Spring Data برای دسترسی به انتیتهای JPA;
- [rdvmedecins.metier]: لایه [métier]؛
- [rdvmedecins.domain]: اِنتِیتیهایی که توسط لایه [métier] مدیریت میشوند؛
- [rdvmdecins.config]: کلاسهای پیکربندی لایه پایداری؛
- [rdvmedecins.boot]: یک برنامه کنسول پایه؛
2.4. پیکربندی Maven
![]() | ![]() | ![]() |
فایل [pom.xml] پروژه به شرح زیر است:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
</dependencies>
<properties>
<!-- برای همه چیز از UTF-8 استفاده کنید -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>istia.st.spring.data.main.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>org.jboss.repository.releases</id>
<name>JBoss Maven Release Repository</name>
<url>https://repository.jboss.org/nexus/content/repositories/releases</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
- خطوط ۸–۱۲: پروژه بر اساس پروژه والد [spring-boot-starter-parent] ساخته شده است. برای وابستگیهایی که از قبل در پروژه والد موجود هستند، هیچ نسخهای مشخص نشده است. از نسخهای که در پروژه والد تعریف شده است استفاده خواهد شد. سایر وابستگیها به صورت عادی اعلام شدهاند؛
- خطوط 14–17: برای Spring Data؛
- خطوط ۱۸–۲۲: برای تستهای JUnit؛
- خطوط ۲۳–۲۶: درایور JDBC برای SGBD و MySQL5;
- خطوط ۲۷–۳۴: استخر اتصال Commons DBCP;
- خطوط ۳۵–۳۸: کتابخانه جکسون برای مدیریت JSON;
- خطوط ۳۹–۴۳: کتابخانه گوگل برای مدیریت مجموعهها؛
نسخهٔ 1.1.0.RC1 از [spring-boot-starter-parent] از نسخههای کتابخانهای زیر استفاده میکند:
2.5. نهادهای JPA
![]() |
اشیاء JPA آنهایی هستند که سطرها را در جداول پایگاه داده در بر میگیرند.
![]() |
کلاس [AbstractEntity] کلاس والدِ اشیاء [Personne, Creneau, Rv] است. تعریف آن به شرح زیر است:
package rdvmedecins.entities;
import java.io.Serializable;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
@MappedSuperclass
public class AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
protected Long id;
@Version
protected Long version;
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
// ابتداییسازی
public AbstractEntity build(Long id, Long version) {
this.id = id;
this.version = version;
return this;
}
@Override
public boolean equals(Object entity) {
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return this.id == other.id;
}
// گیرندهها و تنظیمکنندهها
..
}
- خط ۱۱: انوتیشن [@MappedSuperclass] نشان میدهد که کلاس انوتهشده، والدِ اِنتیتیهای JPA و [@Entity] است؛
- خطوط ۱۵–۱۷: کلید اصلی [id] را برای هر انتها تعریف میکنند. این حاشیهنویسی [@Id] است که فیلد [id] را بهعنوان کلید اصلی تعیین میکند. پادمان [@GeneratedValue(strategy = GenerationType.AUTO)] نشان میدهد که مقدار این کلید اصلی توسط SGBD تولید میشود و هیچ حالت تولید مشخص نشده است؛
- خطوط ۱۸–۱۹: نسخهٔ هر انتها را تعریف میکنند. پیادهسازی JPA هر بار که انتها تغییر کند، این شمارهٔ نسخه را افزایش خواهد داد. این عدد برای جلوگیری از بهروزرسانی همزمان یک موجودیت توسط دو کاربر مختلف استفاده میشود: دو کاربر، U1 و U2، موجودیت E را با شماره نسخه برابر با V1 میخوانند. U1، E را اصلاح میکند و این تغییر را در پایگاه داده ثبت میکند: شماره نسخه سپس به V1+1 تغییر مییابد. به نوبه خود E را اصلاح میکند و این تغییر را در پایگاه داده ثبت میکند: یک استثنا (exception) به وجود خواهد آمد زیرا نسخه آن (V1) با نسخه موجود در پایگاه داده (V1+1) متفاوت است؛
- خطوط ۲۹–۳۳: متد [build] برای مقداردهی اولیه دو فیلد [AbstractEntity] استفاده میشود. این متد مرجع به نمونه [AbstractEntity] را که به این ترتیب مقداردهی شده است، برمیگرداند؛
- خطوط ۳۶–۴۴: متد [equals] کلاس بازتعریف شده است: دو اِنتیتی زمانی برابر در نظر گرفته میشوند که نام کلاس و شناسهی یکسانی داشته باشند؛
کلاس [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] در پایگاه داده؛
- خط 16: موجوده [Rv] از موجوده [AbstractEntity] مشتق شده و بنابراین شناسگر [id] و نسخه [version] را به ارث میبرد؛
- خط ۲۱: تاریخ قرار ملاقات؛
- خط ۲۰: نوع جاوا [Date] شامل هم تاریخ و هم زمان است. در اینجا مشخص شده است که فقط تاریخ استفاده میشود؛
- خطوط ۲۴–۲۶: مشتریای که این قرار ملاقات برای او تنظیم شده است. جدول [RV] دارای یک کلید خارجی است که به جدول [CLIENTS] ارجاع میدهد. این رابطه توسط خطوط ۲۴–۲۶ نشان داده شده است؛
- ردههای ۲۹–۳۱: بازه زمانی قرار ملاقات. جدول [RV] دارای یک کلید خارجی است که به جدول [CRENEAUX] ارجاع میدهد. این رابطه توسط ردههای ۲۹–۳۱ نشان داده شده است؛
- ردههای ۳۴–۳۵: کلید خارجی [idClient];
- ردههای ۳۶–۳۷: کلید خارجی [idCreneau];
2.6. لایه [DAO]
![]() |
ما لایه [DAO] را با استفاده از Spring Data پیادهسازی خواهیم کرد:
![]() |
لایه [DAO] با استفاده از چهار رابط Spring Data پیادهسازی شده است:
- [ClientRepository]: دسترسی به انتیتهای JPA و [Client] را فراهم میکند؛
- [CreneauRepository]: دسترسی به نهادهای JPA و [Creneau] را فراهم میکند؛
- [MedecinRepository]: دسترسی به نهادهای JPA و [Medecin] را فراهم میکند؛
- [RvRepository]: دسترسی به انتیتهای JPA و [Rv] را فراهم میکند؛
رابط [MedecinRepository] به شرح زیر است:
package rdvmedecins.repositories;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Medecin;
public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
- خط ۷: رابط [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é] انجام میشود که درخواست میکند برای بازیابی موجودیت مرجع، یک پیوند با جدولی که کلید خارجی به آن اشاره میکند برقرار شود؛
2.7. لایه [métier]
![]() |
![]() |
- [IMetier] رابط لایه [métier] است و [Metier] پیادهسازی آن است؛
- [AgendaMedecinJour] و [CreneauMedecinJour] دو موجودیت کسبوکار هستند؛
2.7.1. این اشیاء
این موجودیت [CreneauMedecinJour] یک بازه زمانی را به هر قرار ملاقاتی که در آن بازه رزرو شده باشد، مرتبط میسازد:
package rdvmedecins.domain;
import java.io.Serializable;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// 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());
}
// گیرندهها و تنظیمکنندهها
...
}
- خط ۱۳: پزشک؛
- خط ۱۴: روز در دفترچه یادداشت؛
- خط ۱۵: بازههای زمانی آنها، با یا بدون قرار ملاقات؛
2.7.2. سرویس
رابط لایه [métier] به شرح زیر است:
package rdvmedecins.metier;
import java.util.Date;
import java.util.List;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
public interface IMetier {
// فهرست مشتریان
public List<Client> getAllClients();
// فهرست پزشکان
public List<Medecin> getAllMedecins();
// فهرست شکافهای نوبتدهی پزشک
public List<Creneau> getAllCreneaux(long idMedecin);
// فهرست نوبتهای پزشک در یک روز مشخص
public List<Rv> getRvMedecinJour(long idMedecin, Date jour);
// پیدا کردن مشتری بر اساس شناسه آن
public Client getClientById(long id);
// پیدا کردن یک مشتری با شناسهٔ آن
public Medecin getMedecinById(long id);
//یافتن یک قرار ملاقات با شناسهٔ آن
public Rv getRvById(long id);
// پیدا کردن یک بازه زمانی با شناسه آن
public Creneau getCreneauById(long id);
// افزودن یک RV
public Rv ajouterRv(Date jour, Creneau créneau, Client client);
// حذف یک RV
public void supprimerRv(Rv rv);
// خط کسبوکار
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);
}
توضیحات، نقش هر متد را توضیح میدهند.
پیادهسازی رابط [IMetier] کلاس زیر [Metier] است:
package rdvmedecins.metier;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.domain.CreneauMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.repositories.ClientRepository;
import rdvmedecins.repositories.CreneauRepository;
import rdvmedecins.repositories.MedecinRepository;
import rdvmedecins.repositories.RvRepository;
import com.google.common.collect.Lists;
@Service("métier")
public class Metier implements IMetier {
// انبارها
@Autowired
private MedecinRepository medecinRepository;
@Autowired
private ClientRepository clientRepository;
@Autowired
private CreneauRepository creneauRepository;
@Autowired
private RvRepository rvRepository;
//پیادهسازی رابط
@Override
public List<Client> getAllClients() {
return Lists.newArrayList(clientRepository.findAll());
}
@Override
public List<Medecin> getAllMedecins() {
return Lists.newArrayList(medecinRepository.findAll());
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return Lists.newArrayList(creneauRepository.getAllCreneaux(idMedecin));
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return Lists.newArrayList(rvRepository.getRvMedecinJour(idMedecin, jour));
}
@Override
public Client getClientById(long id) {
return clientRepository.findOne(id);
}
@Override
public Medecin getMedecinById(long id) {
return medecinRepository.findOne(id);
}
@Override
public Rv getRvById(long id) {
return rvRepository.findOne(id);
}
@Override
public Creneau getCreneauById(long id) {
return creneauRepository.findOne(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau créneau, Client client) {
return rvRepository.save(new Rv(jour, client, créneau));
}
@Override
public void supprimerRv(Rv rv) {
rvRepository.delete(rv.getId());
}
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
...
}
}
- خط ۲۴: آناوتیشن [@Service] یک آناوتیشن Spring است که کلاس دارای آناوتیشن را به یک کامپوننت مدیریتشده توسط Spring تبدیل میکند. یک کامپوننت ممکن است نامگذاری شود یا نشود. این مورد [métier] نام دارد؛
- خط ۲۵: کلاس [Metier] رابط [IMetier] را پیادهسازی میکند؛
- خط ۲۸: آناوتیشن [@Autowired] یک آناوتیشن اسپرینگ است. مقدار فیلدی که به این شکل آناوت شده است توسط اسپرینگ با یک مرجع به یک کامپوننت اسپرینگ از نوع یا نام مشخص، اولیه (inject) خواهد شد. در اینجا، آنوتیشن [@Autowired] نامی را مشخص نمیکند. بنابراین، تزریق مبتنی بر نوع انجام خواهد شد؛
- خط ۲۹: فیلد [medecinRepository] با یک مرجع به یک کامپوننت Spring از نوع [MedecinRepository] مقداردهی اولیه خواهد شد. این مرجع، به کلاس تولید شده توسط Spring Data برای پیادهسازی رابط [MedecinRepository] که قبلاً در مورد آن بحث کردهایم، اشاره خواهد داشت؛
- خطوط ۳۰–۳۵: این فرآیند برای سه رابط دیگر مورد بررسی نیز تکرار میشود؛
- خطوط 39–41: پیادهسازی متد [getAllClients]؛
- خط ۴۰: ما از متد [findAll] در اینترفیس [ClientRepository] استفاده میکنیم. این متد یک نوع [Iterable<Client>] را برمیگرداند که ما آن را با استفاده از متد استاتیک [Lists.newArrayList] به [List<Client>] تبدیل میکنیم. کلاس [Lists] در کتابخانه 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;
}
از خوانندگان دعوت میشود نظرات را مطالعه کنند. الگوریتم به شرح زیر است:
- تمام بازههای زمانی برای پزشک مشخصشده بازیابی میشوند؛
- تمام قرارهای ملاقات آنها را برای روز مشخص بازیابی میکنیم؛
- با استفاده از این دو اطلاعات، میتوانیم تشخیص دهیم که یک بازه زمانی آزاد است یا رزرو شده است؛
2.8. پیکربندی پروژه
![]() |
کلاس [DomainAndPersitenceConfig] کل پروژه را پیکربندی میکند:
package rdvmedecins.config;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.orm.jpa.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
//منبع داده MySQL
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
dataSource.setUsername("root");
dataSource.setPassword("");
return dataSource;
}
//ارائهدهنده JPA – در صورتی که از مقادیر پیشفرض Spring Boot راضی هستید، نیازی به آن نیست
// در اینجا آن را برای فعال/غیرفعال کردن لاگگیری تعریف میکنیم SQL
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
return hibernateJpaVendorAdapter;
}
//EntityManagerFactory و TransactionManager توسط Spring Boot روی مقادیر پیشفرض تنظیم شدهاند
}
- خط ۴۵: ما بیینهای [EntityManagerFactory] و [TransactionManager] را تعریف نخواهیم کرد. در عوض، ما به anotation Spring Boot [@EnableAutoConfiguration] (خط ۱۷) متکی خواهیم بود؛
- خطوط 24–32: منبع داده MySQL5 را تعریف میکنیم. این یک بین است که Spring Boot عموماً نمیتواند آن را استنباط کند؛
- خطوط ۳۶–۴۳: ما همچنین پیادهسازی JPA را پیکربندی میکنیم تا ویژگی Hibernate [showSql] را روی false تنظیم کند (خط ۳۹). بهطور پیشفرض، این ویژگی روی true تنظیم شده است؛
- در حال حاضر، تنها مؤلفههایی که توسط Spring مدیریت میشوند، بیینهای موجود در خطوط ۲۵ و ۳۷ و همچنین بیینهای [EntityManagerFactory] و [TransactionManager] از طریق پیکربندی خودکار هستند. ما باید بیینهای لایههای [métier] و [DAO] را اضافه کنیم؛
- خط ۱۶، رابطهای موجود در بسته [rdvmdecins.repositories] را که از رابط [CrudRepository] ارثبری میکنند، به کانکست Spring اضافه میکند؛
- خط ۱۸ تمام کلاسهای موجود در بسته [rdvmedecins] و زیرکلاسهای آن را که دارای انوتیشن Spring هستند، به کانکست Spring اضافه میکند. در پکیج [rdvmdecins.metier]، کلاس [Metier]، با آنوتیشن [@Service] خود، پیدا شده و به کانکست Spring اضافه میشود؛
- خط ۴۵: یک بیون [entityManagerFactory] بهطور پیشفرض توسط Spring Boot تعریف خواهد شد. باید به این بیون گفته شود که اِنتیتِیهای JPA که قرار است مدیریت کند، در کجا قرار دارند. خط ۱۹ این کار را انجام میدهد؛
- خط ۲۰: مشخص میکند که متدهای رابطهایی که از رابط [CrudRepository] ارث میبرند، باید در داخل یک تراکنش اجرا شوند؛
2.9. تستها برای لایه [métier]
کلاس [rdvmedecins.tests.Metier] یک کلاس تست Spring است / JUnit 4:
package rdvmedecins.tests;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Metier {
@Autowired
private IMetier métier;
@Test
public void test1(){
//نمایش مشتری
List<Client> clients = métier.getAllClients();
display("Liste des clients :", clients);
//نمای پزشکان
List<Medecin> medecins = métier.getAllMedecins();
display("Liste des médecins :", medecins);
//نمایش اسلاتهای قرار ملاقات پزشک
Medecin médecin = medecins.get(0);
List<Creneau> creneaux = métier.getAllCreneaux(médecin.getId());
display(String.format("Liste des créneaux du médecin %s", médecin), creneaux);
//فهرست قرار ملاقاتهای یک پزشک در یک روز مشخص
Date jour = new Date();
display(String.format("Liste des rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// افزودن یک RV
Rv rv = null;
Creneau créneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
rv = métier.ajouterRv(jour, créneau, client);
// تأیید
Rv rv2 = métier.getRvById(rv.getId());
Assert.assertEquals(rv, rv2);
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// افزودن یک RV به همان شکاف در همان روز
//باید یک استثنا را فعال کند
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
Boolean erreur = false;
try {
rv = métier.ajouterRv(jour, créneau, client);
System.out.println("Rv ajouté");
} catch (Exception ex) {
Throwable th = ex;
while (th != null) {
System.out.println(ex.getMessage());
th = th.getCause();
}
// خطا ثبت میشود
erreur = true;
}
//بررسی میکند که خطایی رخ داده است
Assert.assertTrue(erreur);
//فهرست RV
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// نمایش تقویم
AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
System.out.println(agenda);
Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
// حذف یک RV
System.out.println("Suppression du Rv ajouté");
métier.supprimerRv(rv);
// تأیید
rv2 = métier.getRvById(rv.getId());
Assert.assertNull(rv2);
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
}
// روش ابزار – موارد را در یک مجموعه نمایش میدهد
private void display(String message, Iterable<?> elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- خط ۲۲: anotation [@SpringApplicationConfiguration] امکان استفاده از فایل پیکربندی [DomainAndPersistenceConfig] را که قبلاً مورد بحث قرار گرفت، فراهم میکند. بنابراین کلاس تست از تمام beans تعریفشده در این فایل بهرهمند میشود؛
- خط ۲۳: anotasyon [@RunWith] یکپارچهسازی Spring با JUnit را امکانپذیر میسازد: این کلاس قادر خواهد بود بهعنوان یک تست JUnit اجرا شود. [@RunWith] یک آنوتیشن JUnit است (خط ۹)، در حالی که کلاس [SpringJUnit4ClassRunner] یک کلاس Spring است (خط ۱۲);
- خطوط ۲۶–۲۷: تزریق یک مرجع به لایه [métier] به کلاس تست؛
- بسیاری از تستها صرفاً بررسیهای بصری هستند:
- خطوط ۳۲–۳۳: فهرست مشتریان؛
- خطوط ۳۵–۳۶: فهرست پزشکان؛
- خطوط ۳۹–۴۰: فهرست اسلاتهای قرار ملاقات یک پزشک؛
- خط ۴۳: فهرست قرارهای ملاقات پزشک؛
- خط ۵۰: افزودن یک قرار ملاقات جدید. متد [ajouterRv] قرار ملاقات را همراه با اطلاعات اضافی، یعنی کلید اصلی آن «id»، بازمیگرداند؛
- خط ۵۳: این کلید اصلی برای جستجو در پایگاه داده برای یافتن قرار ملاقات استفاده میشود؛
- خط ۵۴: ما بررسی میکنیم که قرار ملاقاتی که جستجو شده و قرار ملاقاتی که پیدا شده یکسان هستند. توجه باید داشت که متد [equals] از انتیت [Rv] مجدداً تعریف شده است: دو قرار ملاقات زمانی برابر در نظر گرفته میشوند که id یکسانی داشته باشند. در اینجا، این به ما نشان میدهد که قرار ملاقات اضافه شده واقعاً در پایگاه داده درج شده است؛
- خطوط ۶۱–۷۳: تلاشی برای افزودن همان قرار ملاقات برای بار دوم انجام میشود. این باید توسط 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 را بازیابی کردهایم، که بدین ترتیب نشان میدهد قرار ملاقاتی که به دنبالش بودیم وجود ندارد؛
آزمون با موفقیت اجرا میشود:
![]() |
2.10. برنامه کنسول
![]() |
برنامه کنسول ساده است. این برنامه نشان میدهد چگونه یک کلید خارجی را بازیابی کنیم:
package rdvmedecins.boot;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
public class Boot {
// راهاندازی
public static void main(String[] args) {
// آمادهسازی پیکربندی
SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
app.setLogStartupInfo(false);
// راهاندازی آن
ConfigurableApplicationContext context = app.run(args);
//منطق کسبوکار
IMetier métier = context.getBean(IMetier.class);
try {
// افزودن یک RV
Date jour = new Date();
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new SimpleDateFormat("dd/MM/yyyy").format(jour)));
Client client = (Client) new Client().build(1L, 1L);
Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
Rv rv = métier.ajouterRv(jour, créneau, client);
System.out.println(String.format("Rv ajouté = %s", rv));
// تأیید
créneau = métier.getCreneauById(1L);
long idMedecin = créneau.getIdMedecin();
display("Liste des rendez-vous", métier.getRvMedecinJour(idMedecin, jour));
} catch (Exception ex) {
System.out.println("Exception : " + ex.getCause());
}
//بستن زمینه Spring
context.close();
}
// متد ابزار – عناصر یک مجموعه را نمایش میدهد
private static <T> void display(String message, Iterable<T> elements) {
System.out.println(message);
for (T element : elements) {
System.out.println(element);
}
}
}
برنامه یک قرار ملاقات اضافه میکند و سپس بررسی میکند که آیا اضافه شده است یا خیر.
- خط ۱۹: کلاس [SpringApplication] از کلاس پیکربندی [DomainAndPersistenceConfig] استفاده خواهد کرد؛
- خط ۲۰: حذف لاگهای راهاندازی برنامه؛
- خط ۲۲: کلاس [SpringApplication] اجرا میشود. این کلاس یک کانکست Spring را بازمیگرداند، یعنی لیست Beans ثبتشده؛
- خط ۲۴: یک مرجع به باینی که رابط [IMetier] را پیادهسازی میکند، بازیابی میشود. بنابراین این یک مرجع به لایه [métier] است؛
- خطوط ۲۷–۳۱: یک قرار ملاقات جدید برای امروز، برای مشتری شماره ۱ در نوبت شماره ۱ اضافه میشود. مشتری و نوبت از ابتدا ایجاد شدهاند تا نشان دهند که فقط شناسهها استفاده میشوند. نسخه در اینجا مقداردهی اولیه شده است، اما میتوانستیم هر عددی را وارد کنیم. این عدد در اینجا استفاده نمیشود؛
- خط ۳۴: میخواهیم بفهمیم کدام پزشک شیفت شماره ۱ را دارد. برای این کار، باید از پایگاه داده برای شیفت شماره ۱ پرسوجو کنیم. از آنجا که در حالت [FetchType.LAZY] هستیم، پزشک همراه با شیفت بازنمیگردد. با این حال، ما با دقت یک فیلد [idMedecin] را در درون انتیت [Creneau] قرار دادهایم تا کلید اصلی دکتر را بازیابی کنیم؛
- خط ۳۵: شناسهٔ اصلی پزشک را بازیابی کنید؛
- خط ۳۶: نمایش فهرست قرارهای ملاقات پزشک؛
خروجی کنسول به شرح زیر است:
2.11. مقدمهای بر اسپرینگ MVC
![]() |
اکنون به ساخت لایه وب میپردازیم. این لایه عمدتاً شامل متدهایی است که URL را پردازش کرده و با یک خط متن در قالب JSON (نمایش شیء جاوااسکریپت) پاسخ میدهند. این لایه وب یک رابط کاربری وب است که گاهی به آن وب API گفته میشود. ما این رابط را با استفاده از Spring MVC، شاخهای دیگر از اکوسیستم Spring، پیادهسازی خواهیم کرد. ابتدا با مطالعه یکی از راهنماهای موجود در [http://spring.io] شروع میکنیم.
2.11.1. پروژهٔ نمایشی
![]() |
- در [1]، یکی از راهنماهای Spring را وارد میکنیم؛
![]() |
- در [2]، مثال [Rest Service] را انتخاب میکنیم؛
- در [3]، پروژه Maven را انتخاب میکنیم؛
- در [4]، نسخه نهایی راهنما را انتخاب میکنیم؛
- در [5] تأیید میکنیم؛
- در [6]، پروژه واردشده؛
خدمات وبی که از طریق استاندارد URL قابل دسترسی هستند و متن JSON را بازمیگردانند، اغلب به عنوان خدمات REST (انتقال وضعیت REpresentational) شناخته میشوند. در این سند، من به سادگی سرویسی را که قصد ساخت آن را داریم، «سرویس وب / JSON» مینامم. یک سرویس زمانی RESTful نامیده میشود که از قوانین خاصی پیروی کند. من تلاش نکردهام که به این قوانین پایبند باشم.
اکنون بیایید پروژه واردشده را بررسی کنیم، با پیکربندی Maven آن شروع میکنیم.
2.11.2. پیکربندی Maven
فایل [pom.xml] به شرح زیر است:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-releases</id>
<url>http://repo.spring.io/release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<url>http://repo.spring.io/release</url>
</pluginRepository>
</pluginRepositories>
</project>
- خطوط ۱۰–۱۴: همانند پروژه [Spring Data]، پروژه والد [Spring Boot] فهرست شده است؛
- خطوط 17–20: آرتیفکت [spring-boot-starter-web] شامل کتابخانههای مورد نیاز برای یک پروژه Spring، MVC است. به طور خاص، این شامل یک سرور Tomcat تعبیهشده است. برنامه روی این سرور اجرا خواهد شد؛
- خطوط 21–24: کتابخانه Jackson تبدیل یک شیء جاوا به رشته و بالعکس را مدیریت میکند؛
این پیکربندی شامل تعداد زیادی کتابخانه است:
![]() | ![]() |
در بالا سه آرشیو روی سرور Tomcat آمده است.
2.11.3. معماری یک سرویس Spring REST
اسپرینگ MVC الگوی معماری موسوم به MVC (مدل–نما–کنترلکننده) را به شرح زیر پیادهسازی میکند:
![]() |
پردازش یک درخواست مشتری به شرح زیر انجام میشود:
- درخواست – URL درخواستی به شکل http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... است. [Dispatcher Servlet] کلاسی از Spring است که URL ورودی را پردازش میکند. این URL را به اکشنی که باید آن را پردازش کند، مسیریابی میکند. این اکشنها متدهایی از کلاسهای خاصی به نام [Contrôleurs] هستند. «C» در MVC در این مورد، رشته [Dispatcher Servlet, Contrôleur, Action] است. اگر هیچ اکشنی برای رسیدگی به URL ورودی پیکربندی نشده باشد، سروِلِت [Dispatcher Servlet] پاسخ خواهد داد که URL درخواستی یافت نشد (خطای 404 NOT FOUND);
- پردازش
- عملکرد انتخابشده میتواند از پارامترهای parami که توسط سرولت [Dispatcher Servlet] به آن ارسال شده است، استفاده کند. این پارامترها ممکن است از چندین منبع منشأ بگیرند:
- مسیر [/param1/param2/...] از URL،
- پارامترهای [p1=v1&p2=v2] از URL,
- پارامترهای ارسالشده توسط مرورگر در درخواست آن؛
- هنگام پردازش درخواست کاربر، ممکن است اقدام به لایههای [metier] و [2b] نیاز داشته باشد. پس از پردازش درخواست مشتری، ممکن است پاسخهای مختلفی ایجاد شود. یک مثال کلاسیک این است:
- یک صفحهٔ خطا اگر درخواست نتوانست به درستی پردازش شود
- در غیر این صورت، یک صفحه تأیید
- اقدام دستور نمایش یک نمای مشخص را صادر میکند [3]. این نما دادههایی را که به عنوان مدل نما شناخته میشوند، نمایش میدهد. این همان M در MVC است. اقدام این مدل M [2c] را ایجاد کرده و به یک نما V دستور نمایش را میدهد [3]؛
- پاسخ – نمای انتخابشده V از مدل M ساختهشده توسط اکشن برای راهاندازی بخشهای پویا از پاسخی که باید به کلاینت ارسال کند استفاده میکند، و سپس این پاسخ را ارسال میکند.
برای یک سرویس وب / JSON، معماری قبلی کمی تغییر میکند:
![]() |
- در [4a]، مدل—که یک کلاس جاوا است—توسط یک کتابخانه JSON به یک رشته JSON تبدیل میشود؛
- در [4b]، این رشته JSON به مرورگر ارسال میشود؛
2.11.4. C
![]() |
برنامه واردشده دارای کنترلر زیر است:
package hello;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
@RequestMapping("/greeting")
public @ResponseBody
Greeting greeting(@RequestParam(value = "name", required = false, defaultValue = "World") String name) {
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}
}
- خط ۹: حاشیهنویسی [@Controller] کلاس [GreetingController] را به یک کنترلکننده Spring تبدیل میکند، یعنی متدهای آن برای رسیدگی به URL ثبت شدهاند؛
- خط ۱۵: آناوتیشن [@RequestMapping] متدی را که متد کنترل میکند مشخص میکند، در این مورد URL [/greeting]. بعداً خواهیم دید که این URL قابل پیکربندی است و بازیابی این تنظیمات امکانپذیر است؛
- خط ۱۶: انوتیشن [@ResponseBody] نشان میدهد که متد برای یک ویو (JSP, JSF, Thymeleaf، ...) که سپس به مرورگر مشتری ارسال میشود، اما در عوض پاسخ را مستقیماً به خود مرورگر تولید میکند. در اینجا، یک شیء از نوع [Greeting] (خط ۱۸) تولید میکند. اگرچه در اینجا مشخص نیست، این شیء قبل از ارسال به مرورگر ابتدا به JSON تبدیل میشود. وجود یک کتابخانه JSON در میان وابستگیهای پروژه باعث میشود که Spring Boot پروژه را از طریق پیکربندی خودکار به این شکل پیکربندی کند؛
- خط 17: متد [greeting] دارای پارامتری به نام [String name] است. آنگت [@RequestParam(value = "name", required = false, defaultValue = "World"] نشان میدهد که این پارامتر باید با پارامتر نام [name](@RequestParam(value = "name"). این ممکن است پارامتری از GET یا POST باشد. این پارامتر الزامی نیست (required = false). در مورد دوم، پارامتر [name] متد با مقدار [World] (defaultValue = "World") مقداردهی اولیه خواهد شد.
2.11.5. مدل M
مدل M تولیدشده توسط روش قبلی، شیء زیر [Greeting] است:
![]() |
package hello;
public class Greeting {
private final long id;
private final String content;
public Greeting(long id, String content) {
this.id = id;
this.content = content;
}
public long getId() {
return id;
}
public String getContent() {
return content;
}
}
تبدیل JSON این شیء رشتهٔ کاراکتری {"id":n,"content":"text"} را ایجاد خواهد کرد. در نهایت، رشتهای که توسط متد کنترلکننده تولید میشود، به شکل زیر خواهد بود:
یا
2.11.6. پیکربندی پروژه
![]() |
پروژه توسط کلاس زیر پیکربندی میشود: [Application]:
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- خط ۱۱: جالب اینجاست که این کلاس از طریق یک متد ([main]) که مخصوص برنامههای کنسول است، قابل اجرا است. و این دقیقاً همینطور است. کلاس [SpringApplication] در خط ۱۲، سرور Tomcat موجود در وابستگیها را راهاندازی کرده و سرویس REST را روی آن مستقر میکند؛
- خط ۴: میبینیم که کلاس [SpringApplication] به پروژه [Spring Boot] تعلق دارد؛
- خط ۱۲: پارامتر اول کلاسی است که پروژه را پیکربندی میکند؛ پارامتر دوم حاوی هرگونه پارامتر اضافی است؛
- خط ۸: انوتیشن [@EnableAutoConfiguration] به Spring Boot دستور میدهد تا پروژه را پیکربندی کند؛
- خط ۷: آناوتیشن [@ComponentScan] باعث میشود پوشهای که حاوی کلاس [Application] است برای کامپوننتهای Spring اسکن شود. یکی یافت میشود: کلاس [GreetingController] که دارای انوتیشن [@Controller] است و آن را به یک کامپوننت اسپرینگ تبدیل میکند؛
2.11.7. اجرای پروژه
بیایید پروژه را اجرا کنیم:
![]() |
خروجیهای کنسول زیر تولید میشوند:
____ _ __ _ _
- خط ۱۲: سرور Tomcat روی پورت ۸۰۸۰ شروع میشود (خط ۱۱);
- خط 16: سرویسلت [DispatcherServlet] موجود است؛
- خط ۱۹: متد [GreetingController.greeting] کشف شده است؛
برای آزمایش برنامه وب، ما URL و [http://localhost:8080/greeting] را درخواست میکنیم:
![]() | ![]() |
ما در واقع رشتهٔ مورد انتظار JSON را دریافت میکنیم. ممکن است مشاهدهٔ سربرگهای ارسالشده توسط سرور جالب باشد. برای این کار، از افزونهٔ کروم به نام [Advanced Rest Client] (رجوع شود به پیوستها) استفاده خواهیم کرد:
![]() |
- در [1]، URL درخواستشده؛
- در [2]، از روش GET استفاده میشود؛
- در [3]، پاسخ JSON است؛
- در [4]، سرور اعلام کرد که در حال ارسال پاسخی با فرمت JSON است؛
- در [5]، همان URL درخواست میشود، اما این بار با POST؛
- در [7]، اطلاعات به شکل [urlencoded] به سرور ارسال میشود؛
- در [6]، پارامتر 'name' به همراه مقدار آن؛
- در [8]، مرورگر به سرور اطلاع میدهد که در حال ارسال اطلاعات به آن است [urlencoded];
- در [9]، پاسخ سرور JSON;
2.11.8. ایجاد یک آرشیو قابل اجرا
امکان ایجاد یک آرشیو قابل اجرا خارج از Eclipse وجود دارد. پیکربندی لازم در فایل [pom.xml] قرار دارد:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.Application</start-class>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- خطوط ۹–۱۲ افزونهای را که آرشیو قابل اجرا را ایجاد میکند، تعریف میکنند؛
- خط ۳ کلاس اجرایی پروژه را تعریف میکند؛
طبق مراحل زیر عمل کنید:
![]() |
- در [1]: یک هدف Maven اجرا میشود؛
- در [2]: دو هدف وجود دارد: [clean] برای حذف پوشه [target] از پروژه Maven، و [package] برای بازتولید آن؛
- در [3]: پوشه تولید شده [target] در این پوشه ایجاد خواهد شد؛
- در [4]: هدف تولید میشود؛
در لاگهایی که در کنسول ظاهر میشوند، مهم است که پلاگین [spring-boot-maven-plugin] را در فهرست ببینید. این پلاگین است که آرشیو قابل اجرا را تولید میکند.
با استفاده از کنسول، به پوشهٔ تولیدشده بروید:
- خط ۵: آرشیو تولید شده؛
این آرشیو به شرح زیر اجرا میشود:
اکنون که برنامه وب راهاندازی شده است، میتوانید با استفاده از یک مرورگر به آن دسترسی پیدا کنید:
![]() |
2.11.9. استقرار برنامه روی سرور Tomcat
در حالی که Spring Boot در حالت توسعه بسیار کاربردی است، احتمالاً یک برنامه در محیط تولید بر روی یک سرور Tomcat واقعی مستقر خواهد شد. در اینجا نحوه انجام آن آمده است:
فایل [pom.xml] را به شرح زیر ویرایش کنید:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<packaging>war</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
....
</project>
تغییراتی باید در دو مکان اعمال شود:
- خط ۹: باید مشخص کنید که یک آرشیو WAR (وب ARchive) باید تولید شود؛
- خطوط ۲۶–۳۰: وابستگی به آرتفکت [spring-boot-starter-tomcat] را اضافه کنید. این آرتفکت شامل تمام کلاسهای Tomcat در وابستگیهای پروژه است؛
- خط ۲۹: این آرتفکت [provided] است، به این معنی که آرشیوهای متناظر در WAR تولید شده گنجانده نخواهند شد. در عوض، این آرشیوها روی سرور Tomcat که برنامه روی آن اجرا میشود، قرار خواهند گرفت؛
برنامه وب نیز باید پیکربندی شود. در صورت عدم وجود فایل [web.xml]، این کار با استفاده از کلاسی که از [SpringBootServletInitializer] ارث میبرد، انجام میشود:
![]() |
کلاس [ApplicationInitializer] به شرح زیر است:
package hello;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
public class ApplicationInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
- خط ۶: کلاس [ApplicationInitializer] از کلاس [SpringBootServletInitializer] ارث میبرد؛
- خط ۹: متد [configure] بازتعریف شده است (خط ۸);
- خط ۱۰: کلاسی که پروژه را پیکربندی میکند مشخص شده است؛
برای اجرای پروژه، مراحل زیر را دنبال کنید:
![]() |
- در [1]، پروژه را روی یکی از سرورهای ثبتشده در Eclipse در IDE اجرا کنید؛
- در [2]، [tc Server Developer] را که گزینه پیشفرض است، انتخاب کنید. این یک نسخه از Tomcat است؛
پس از انجام این کار، میتوانید در یک مرورگر به URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] دسترسی پیدا کنید:
![]() |
اکنون میدانیم چگونه یک آرشیو WAR تولید کنیم. در ادامه، کار با Spring Boot و آرشیو قابل اجرای JAR آن را ادامه خواهیم داد.
2.11.10. ایجاد یک پروژه وب جدید
برای ایجاد یک پروژه وب جدید، مراحل زیر را دنبال کنید:
![]() |
- در [1]: File / New / Spring Starter Project
- در [2]: [Web] را انتخاب کنید. هیچ کتابخانه ویوی را انتخاب نکنید، زیرا در یک سرویس وب / JSON، هیچ ویوی وجود ندارد؛
- پروژهای که ایجاد میشود یک پروژه Maven خواهد بود. در [3]، گروه (group) برای آرتیفکت (artifact) Maven که قرار است ایجاد شود را وارد کنید؛ در [4]، نام آرتیفکت را وارد کنید؛
- در [5]، نام پکیجی را که Spring کلاس پیکربندی پروژه را در آن قرار میدهد، وارد کنید؛
- در [6]، شما نامی برای پروژهٔ Eclipse وارد میکنید – این نام ممکن است با [4] متفاوت باشد؛
![]() |
2.12. لایه [web]
![]() |
![]() |
ما لایه وب را در چند مرحله خواهیم ساخت:
- مرحلهٔ ۱: یک لایهٔ وب عملیاتی بدون احراز هویت؛
- مرحله ۲: پیادهسازی احراز هویت با استفاده از 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 خواهد بود که لزوماً به همان دامنه سرویس وب ما تعلق ندارد. بنابراین، به طور پیشفرض، این کلاینت نمیتواند به سرویس وب دسترسی داشته باشد، مگر اینکه مجاز به انجام این کار باشد. ما خواهیم دید چگونه؛
2.12.1. پیکربندی Maven
فایل [pom.xml] پروژه به شرح زیر است:
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.mvc</groupId>
<artifactId>rdvmedecins-webapi-v1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-webapi-v1</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
- خطوط ۷–۱۱: پروژهٔ والد Maven؛
- خطوط ۱۳–۱۶: وابستگیها برای یک پروژه Spring، MVC;
- خطوط 17–21: وابستگیها به پروژه لایه [métier, DAO, JPA]؛
2.12.2. رابط سرویس وب
![]() |
- در [1]، همانطور که در بالا نشان داده شده است، مرورگر فقط میتواند تعداد محدودی از URL را با یک سینتکس خاص درخواست کند؛
- در [4]، یک پاسخ JSON دریافت میکند؛
پاسخهای سرویس وب ما همگی دارای فرمت یکسانی خواهند بود که مطابق با تبدیل JSON یک شیء از نوع [Reponse] به شرح زیر است:
package rdvmedecins.web.models;
public class Reponse {
// ----------------- ویژگیها
//وضعیت تراکنش
private int status;
// پاسخ JSON
private Object data;
// ---------------سازندهها
public Reponse() {
}
public Reponse(int status, Object data) {
this.status = status;
this.data = data;
}
// روشها
public void incrStatusBy(int increment) {
status += increment;
}
// ----------------------گیرندهها و تنظیمکنندهها
...
}
- خط ۷: کد خطای پاسخ ۰: 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. فیلد [data] حاوی فرمت JSON قرار ملاقات اضافه شده است؛
وجود قرار ملاقات جدید قابل تأیید است:
![]() |
حذف قرار ملاقات [/supprimerRv]
![]() |
- در [1]، URL از سرویس وب؛
- در [2]، از متد POST استفاده میشود؛
- در [3]، متن JSON اطلاعات ارسالشده به سرویس وب در قالب {idRv} است؛
- در [4]، کلاینت به سرویس وب اطلاع میدهد که در حال ارسال اطلاعات JSON برای آن است؛
پاسخ سپس به شرح زیر است:
![]() |
- در [5]: فیلد [status] روی 0 تنظیم شده است، که بدین ترتیب موفقیت عملیات را نشان میدهد؛
حذف قرار ملاقات را میتوان تأیید کرد:
![]() |
در بالا، قرار ملاقات بیمار [Mme GERMAN] دیگر وجود ندارد.
سرویس وب همچنین امکان بازیابی اشیاء را بر اساس شناسه آنها فراهم میکند:
![]() |
![]() |
![]() |
![]() |
تمام این موجودیتهای URL توسط کنترلکننده [RdvMedecinsController] مدیریت میشوند که اکنون آن را شرح خواهیم داد.
2.12.3. ساختار کنترلکننده [RdvMedecinsController]
![]() |
کنترلکننده [RdvMedecinsController] به شرح زیر است:
package rdvmedecins.web.controllers;
import java.text.ParseException;
...
@RestController
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
private List<String> messages;
@PostConstruct
public void init() {
// پیامهای خطای برنامه
messages = application.getMessages();
}
// فهرست پزشکان
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
public Reponse getAllMedecins() {
...
}
// فهرست مشتریان
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
public Reponse getAllClients() {
...
}
// فهرست اسلاتهای قرار ملاقات پزشک
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
...
}
// فهرست نوبتهای پزشک
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin,
@PathVariable("jour") String jour) {
...
}
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
public Reponse getClientById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
public Reponse getMedecinById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
public Reponse getRvById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
public Reponse getCreneauById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
...
}
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
...
}
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getAgendaMedecinJour(
@PathVariable("idMedecin") long idMedecin,
@PathVariable("jour") String jour) {
...
}
}
- خط ۶: تگ [@RestController] کلاس [RdvMedecinsController] را بهعنوان یک کنترلر Spring مشخص میکند. علاوه بر این، این بدان معناست که متدهایی که با URL سروکار دارند، پاسخی تولید خواهند کرد که به طور خودکار به JSON تبدیل میشود؛
- خطوط ۹–۱۰: یک شیء از نوع [ApplicationModel] توسط Spring در اینجا تزریق خواهد شد؛
- خط ۱۳: آناوتیشن [@PostConstruct] نشاندهنده متدی است که بلافاصله پس از instantiate شدن کلاس اجرا میشود. هنگامی که این متد اجرا میشود، اشیاء تزریقشده توسط Spring در دسترس هستند؛
- تمام متدها یک شیء از نوع [Reponse] را به شرح زیر برمیگردانند:
package rdvmedecins.web.models;
public class Reponse {
// ----------------- ویژگیها
//وضعیت عملیات
private int status;
// پاسخ
private Object data;
...
}
این شیء قبل از ارسال به مرورگر کلاینت، به JSON سریالیزه میشود؛
- خط ۲۰: آناوتیشن [@RequestMapping] شرایط فراخوانی متد را مشخص میکند. در اینجا، متد یک درخواست GET را از URL [/getAllMedecins] پردازش میکند. اگر این URL توسط یک POST درخواست میشد، رد میشد و Spring MVC یک کد خطا HTTP را به کلاینت وب ارسال میکرد؛
- خط ۳۲: URL توسط {idMedecin} پیکربندی میشود. این پارامتر با استفاده از anotation [@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 ارسال شده توسط کلاینت وب به یک شی از نوع [PostAjouter] دیسریالایز خواهد شد. این به شرح زیر است:
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;
// گیرندهها و تنظیمکنندهها
...
}
2.12.4. قالبهای سرویس وب
![]() |
ما قبلاً مدلهای [Reponse, PostAjouterRv, PostSupprimerRv] را ارائه کردهایم. مدل [ApplicationModel] به شرح زیر است:
package rdvmedecins.web.models;
import java.util.Date;
...
@Component
public class ApplicationModel implements IMetier {
//لایه [métier]
@Autowired
private IMetier métier;
//دادهها از لایه [métier]
private List<Medecin> médecins;
private List<Client> clients;
// پیامهای خطا
private List<String> messages;
@PostConstruct
public void init() {
//بازیابی پزشکان و مشتریان
try {
médecins = métier.getAllMedecins();
clients = métier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
// گیرنده
public List<String> getMessages() {
return messages;
}
// ------------------------- رابط لایه [métier]
@Override
public List<Client> getAllClients() {
return clients;
}
@Override
public List<Medecin> getAllMedecins() {
return médecins;
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return métier.getAllCreneaux(idMedecin);
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return métier.getRvMedecinJour(idMedecin, jour);
}
@Override
public Client getClientById(long id) {
return métier.getClientById(id);
}
@Override
public Medecin getMedecinById(long id) {
return métier.getMedecinById(id);
}
@Override
public Rv getRvById(long id) {
return métier.getRvById(id);
}
@Override
public Creneau getCreneauById(long id) {
return métier.getCreneauById(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
return métier.ajouterRv(jour, creneau, client);
}
@Override
public void supprimerRv(Rv rv) {
métier.supprimerRv(rv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
return métier.getAgendaMedecinJour(idMedecin, jour);
}
}
- خط ۶: انوتیشن [@Component] کلاس [ApplicationModel] را به یک کامپوننت Spring تبدیل میکند. همانند تمام کامپوننتهای Spring که تاکنون دیدهایم (به جز @Controller)، تنها یک شیء از این نوع ایجاد خواهد شد (singleton)؛
- خط ۷: کلاس [ApplicationModel] رابط [IMetier] را پیادهسازی میکند؛
- خطوط ۱۰–۱۱: یک مرجع به لایه [métier] توسط Spring تزریق میشود؛
- خط ۱۹: آناوتیشن [@PostConstruct] تضمین میکند که متد [init] بلافاصله پس از instantiate شدن کلاس [ApplicationModel] اجرا شود؛
- خطوط ۲۳–۲۴: فهرستهای پزشکان و مشتریان از لایه [métier] بازیابی میشوند؛
- خط ۲۶: اگر یک استثنا رخ دهد، پیامهای موجود در پشته استثنا در فیلدِ خط ۱۷ ذخیره میشوند؛
کلاس [ApplicationModel] دو هدف را برآورده میکند:
- بهعنوان یک کش برای ذخیرهسازی فهرست پزشکان و بیماران (مشتریان)؛
- بهعنوان یک رابط واحد برای کنترلکنندهها؛
معماری لایه وب به شرح زیر تکامل مییابد:
![]() |
- در [2b]، متدهای کنترلکننده(ها) با نمونهی واحد [ApplicationModel] ارتباط برقرار میکنند؛
این استراتژی انعطافپذیری را در مدیریت کش فراهم میکند. در حال حاضر، اسلاتهای قرار ملاقات پزشکان در کش ذخیره نمیشوند. برای ذخیره کردن آنها در کش، کافی است کلاس [ApplicationModel] را اصلاح کنید. این کار هیچ تأثیری بر کنترلر ندارد و کنترلر همچنان مانند گذشته از متد [List<Creneau> getAllCreneaux(long idMedecin)] استفاده خواهد کرد. آنچه تغییر خواهد کرد، پیادهسازی این متد در کلاس [ApplicationModel] است.
2.12.5. کلاس ایستا
کلاس [Static] حاوی مجموعهای از متدهای کمکی ایستا است که هیچ جنبه «کسبوکاری» یا «وب» ندارند:
![]() |
کد آن به شرح زیر است:
package rdvmedecins.web.helpers;
import java.text.SimpleDateFormat;
...
public class Static {
public Static() {
}
// فهرست پیامهای خطا برای یک استثنا
public static List<String> getErreursForException(Exception exception) {
// فهرست پیامهای خطا برای استثنا را بازیابی میکند
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
erreurs.add(cause.getMessage());
cause = cause.getCause();
}
return erreurs;
}
// نقشهبردارهای شیء --> نقشه
// --------------------------------------------------------
....
}
- خط ۱۲: متد [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()]، میشود.
کلاس [Static] شامل متدهای کمکی دیگری است که هرگاه به آنها برخورد کنیم، به آنها باز خواهیم گشت.
اکنون به تفصیل به پردازش URL در سرویس وب میپردازیم. سه کلاس اصلی در این پردازش دخیل هستند:
- کنترلر [RdvMedecinsController]؛
- کلاس متدهای ابزار [Static]؛
- کلاس کش [ApplicationModel]؛
![]() |
2.12.6. متد [init] کنترلکننده
کنترلکننده [RdvMedecinsController] (بخش 2.12.3 را ببینید) دارای متدی به نام [init] است که بلافاصله پس از ایجاد نمونه آن اجرا میشود:
@Autowired
private ApplicationModel application;
private List<String> messages;
@PostConstruct
public void init() {
// پیامهای خطای برنامه
messages = application.getMessages();
}
- خط ۸: پیامهای خطا که در برنامهٔ [ApplicationModel] کش ذخیره شدهاند، بهصورت محلی در فیلد در خط ۳ ذخیره میشوند. این امکان را برای متدها فراهم میکند تا تشخیص دهند آیا برنامه بهدرستی راهاندازی شده است یا خیر.
2.12.7. URL [/getAllMedecins]
URL [/getAllMedecins] توسط روش زیر از کنترلکننده [RdvMedecinsController] پردازش میشود:
// فهرست پزشکان
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
public Reponse getAllMedecins() {
//وضعیت برنامه
if (messages != null) {
return new Reponse(-1, messages);
}
// فهرست پزشکان
try {
return new Reponse(0, application.getAllMedecins());
} catch (Exception e) {
return new Reponse(1, Static.getErreursForException(e));
}
}
- خط ۵: بررسی میکنیم که آیا برنامه به درستی راهاندازی شده است (messages==null). در غیر این صورت، یک پاسخ با status=-1 و data=messages بازمیگردانیم؛
- خط ۱۰: در غیر این صورت، لیست پزشکان را با یک status برابر با 0 بازمیگردانیم. متد [application.getAllMedecins()] استثنا پرتاب نمیکند زیرا به سادگی یک لیست کششده را بازمیگرداند. با این حال، در صورتی که پزشکان دیگر در کش موجود نباشند، این مدیریت استثنا را حفظ خواهیم کرد؛
ما هنوز سناریویی را که در آن برنامه به اشتباه راهاندازی شده است، بررسی نکردهایم. بیایید SGBD و MySQL5 را متوقف کنیم، سرویس وب را راهاندازی کنیم، و سپس URL و [/getAllMedecins] را درخواست کنیم:

ما در واقع با یک خطا مواجه میشویم. در شرایط عادی، نمای زیر را مشاهده میکردیم:
![]() |
2.12.8. URL [/getAllClients]
URL [/getAllClients] توسط متد زیر از کنترلر [RdvMedecinsController] پردازش میشود:
// فهرست مشتریان
@RequestMapping(value = "/getAllClients")
public Reponse getAllClients() {
//وضعیت درخواست
if (messages != null) {
return new Reponse(-1, messages);
}
// فهرست مشتریان
try {
return new Reponse(0, application.getAllClients());
} catch (Exception e) {
return new Reponse(1, Static.getErreursForException(e));
}
}
این مشابه روش [getAllMedecins] است که قبلاً مورد بحث قرار گرفت. نتایج حاصل به شرح زیر است:
![]() |
2.12.9. URL [/getAllCreneaux/{idMedecin}]
URL و [/getAllCreneaux/{idMedecin}] توسط روش زیرِ کنترلکننده [RdvMedecinsController] پردازش میشوند:
//فهرست اسلاتهای نوبتدهی پزشک
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
//وضعیت درخواست
if (messages != null) {
return new Reponse(-1, messages);
}
// بازیابی پزشک
Reponse réponse = getMedecin(idMedecin);
if (réponse.getStatus() != 0) {
return réponse;
}
Medecin médecin = (Medecin) réponse.getData();
// سلاطهای نوبتدهی پزشک
List<Creneau> créneaux = null;
try {
créneaux = application.getAllCreneaux(médecin.getId());
} catch (Exception e1) {
return new Reponse(3, Static.getErreursForException(e1));
}
// بازگرداندن پاسخ
return new Reponse(0, Static.getListMapForCreneaux(créneaux));
}
- خط ۹: پزشکی که توسط پارامتر [id] شناسایی شده است، از طریق یک متد محلی پرسوجو میشود:
private Reponse getMedecin(long id) {
// در حال بازیابی پزشک
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// پزشک فعلی؟
if (médecin == null) {
return new Reponse(2, null);
}
// باشه
return new Reponse(0, médecin);
}
ما از این متد با یک status در [0,1,2] بازمیگردیم. بیایید به کد متد [getAllCreneaux] بازگردیم:
- خطوط ۱۰–۱۲: اگر status!=0 باشد، پاسخ بلافاصله بازگردانده میشود؛
- خط ۱۳: ما دکتر را جمعآوری میکنیم؛
- خط ۱۷: شیفتهای ملاقات این پزشک را بازیابی کنید؛
- خط ۲۲: یک شیء [Static.getListMapForCreneaux(créneaux)] را بهعنوان پاسخ ارسال میکنیم؛
بیایید تعریف کلاس [Creneau] را به یاد بیاوریم:
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
//ویژگیهای یک اسلات برای RV
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
//یک اسلات به یک دکتر متصل است
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
//کلید خارجی
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
...
}
- خط ۱۳: دکتر در حالت [FetchType.LAZY] جستجو میشود؛
بیایید درخواست JPQL را که متد [getAllCreneaux] را در لایه [DAO] پیادهسازی میکند، به یاد آوریم:
@Query("select c from Creneau c where c.medecin.id=?1")
نشانه [c.medecin.id] پیوندی بین جداول [CRENEAUX] و [MEDECINS] برقرار میکند. در نتیجه، این پرسوجو تمام بازههای زمانی دکتر را بازمیگرداند، بهطوریکه نام دکتر در هر یک از آنها نمایش داده میشود. هنگامی که این بازههای زمانی به JSON سریالیزه میشوند، نام دکتر (JSON) در هر یک از آنها ظاهر میشود. این امر غیرضروری است. بنابراین، به جای سریالسازی یک شیء [Creneau]، یک شیء [Map] را سریالسازی خواهیم کرد که در آن تنها فیلدهای مورد نظر را گنجاندهایم.
بیایید به کدی که قبلاً بررسی کردیم بازگردیم:
// پاسخ بازگردانده میشود
return new Reponse(0, Static.getListMapForCreneaux(créneaux));
متد [Static.getListMapForCreneaux] به شرح زیر است:
// List<Creneau> --> List<Map>
public static List<Map<String, Object>> getListMapForCreneaux(List<Creneau> créneaux) {
// فهرست دیکشنریها <String, Object>
List<Map<String, Object>> liste = new ArrayList<Map<String, Object>>();
for (Creneau créneau : créneaux) {
liste.add(Static.getMapForCreneau(créneau));
}
// فهرست را بازمیگرداند
return liste;
}
و متد [Static.getMapForCreneau] به شرح زیر است:
// Creneau --> Map
public static Map<String, Object> getMapForCreneau(Creneau créneau) {
// کاری هست؟
if (créneau == null) {
return null;
}
// فرهنگ لغت <String, Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", créneau.getId());
hash.put("hDebut", créneau.getHdebut());
hash.put("mDebut", créneau.getMdebut());
hash.put("hFin", créneau.getHfin());
hash.put("mFin", créneau.getMfin());
// ما فرهنگ لغت را بازمیگردانیم
return hash;
}
- خط ۸: یک دیکشنری ایجاد کنید؛
- خطوط ۹–۱۳: فیلدهایی را که میخواهید حفظ کنید به رشته JSON اضافه کنید. فیلد [medecin] شامل نمیشود؛
- خط ۱۵: این دیکشنری بازگردانده میشود؛
نتایج حاصل به شرح زیر است:
![]() |
یا اینها اگر بازه زمانی وجود نداشته باشد:
![]() |
یا اینها در صورت بروز خطای دسترسی به پایگاه داده:
![]() |
2.12.10. URL [/getRvMedecinJour/{idMedecin}/{jour}]
URL و [/getRvMedecinJour/{idMedecin}/{jour}] توسط روش زیر از کنترلکننده [RdvMedecinsController] پردازش میشوند:
// فهرست قرارهای ملاقات دکتر
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
//وضعیت درخواست
if (messages != null) {
return new Reponse(-1, messages);
}
// تاریخ را بررسی کنید
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(3, null);
}
// بازیابی پزشک
Reponse réponse = getMedecin(idMedecin);
if (réponse.getStatus() != 0) {
return réponse;
}
Medecin médecin = (Medecin) réponse.getData();
// فهرست قرار ملاقاتهایشان
List<Rv> rvs = null;
try {
rvs = application.getRvMedecinJour(médecin.getId(), jourAgenda);
} catch (Exception e1) {
return new Reponse(4, Static.getErreursForException(e1));
}
// بازگرداندن پاسخ
return new Reponse(0, Static.getListMapForRvs(rvs));
}
- خط ۳۱: یک شیء `List<Map<String, Object>>` به جای یک شیء `List<Rv>` بازگردانده میشود. تعریف کلاس `[Rv]` را به یاد بیاورید:
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
//جزئیات قرار ملاقات
@Temporal(TemporalType.DATE)
private Date jour;
//یک قرار ملاقات به یک مشتری متصل است
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
//یک قرار ملاقات به یک بازه زمانی متصل است
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// کلیدهای خارجی
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
...
}
- خط ۱۱: مشتری با استفاده از حالت [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 سریال شود؛
واژهنامهای که برای یک قرار ملاقات ساخته میشود به شرح زیر است:
//RV --> نقشه
public static Map<String, Object> getMapForRv(Rv rv) {
// آیا ارتباطی دارد؟
if (rv == null) {
return null;
}
// فرهنگ لغت <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", rv.getId());
hash.put("client", rv.getClient());
hash.put("creneau", getMapForCreneau(rv.getCreneau()));
// بازگرداندن دیکشنری
return hash;
}
- خط ۱۱: ما از دیکشنریِ شیء [Creneau] که قبلاً ارائه کردیم، مجدداً استفاده میکنیم؛
نتایج بهدستآمده به شرح زیر است:
![]() |
یا اینها، با روزی نادرست:
![]() |
یا اینها، با پزشک نادرست:
![]() |
2.12.11. URL و [/getAgendaMedecinJour/{idMedecin}/{jour}]
URL و [/getAgendaMedecinJour/{idMedecin}/{jour}] توسط کنترلر [RdvMedecinsController] با روش زیر پردازش میشوند:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
//وضعیت برنامه
if (messages != null) {
return new Reponse(-1, messages);
}
// تاریخ را بررسی کن
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(3, new String[] { String.format("jour [%s] invalide", jour) });
}
// بازیابی پزشک
Reponse réponse = getMedecin(idMedecin);
if (réponse.getStatus() != 0) {
return réponse;
}
Medecin médecin = (Medecin) réponse.getData();
// دستیابی به دفترچه خاطرات آنها
AgendaMedecinJour agenda = null;
try {
agenda = application.getAgendaMedecinJour(médecin.getId(), jourAgenda);
} catch (Exception e1) {
return new Reponse(4, Static.getErreursForException(e1));
}
// OK
return new Reponse(0, Static.getMapForAgendaMedecinJour(agenda));
}
}
- خط ۳۰، یک شی از نوع List<Map<String, Object>> بازگردانده میشود.
روش [Static.getMapForAgendaMedecinJour] به شرح زیر است:
//AgendaMedecinJour --> نقشه
public static Map<String, Object> getMapForAgendaMedecinJour(AgendaMedecinJour agenda) {
// کاری هست؟
if (agenda == null) {
return null;
}
// فرهنگ لغت <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("medecin", agenda.getMedecin());
hash.put("jour", new SimpleDateFormat("yyyy-MM-dd").format(agenda.getJour()));
List<Map<String, Object>> créneaux = new ArrayList<Map<String, Object>>();
for (CreneauMedecinJour créneau : agenda.getCreneauxMedecinJour()) {
créneaux.add(getMapForCreneauMedecinJour(créneau));
}
hash.put("creneauxMedecin", créneaux);
// ما فرهنگ لغت را بازمیگردانیم
return hash;
}
واژهنامهی ایجادشده سه فیلد دارد:
- [medecin]: پزشکی که صاحب دفترچه است. این اطلاعات حفظ شده است زیرا تنها یک بار ظاهر میشود، در حالی که در موارد قبلی در هر رشته JSON تکرار میشد؛
- [jour]: روز دفترچه یادداشت؛
- [creneauxMedecin]: فهرست بازههای زمانی دکتر، به همراه هر قرار ملاقاتی که برای آن بازه زمانبندی شده است؛
روش [getMapForCreneauMedecinJour] که در خط ۱۳ استفاده شده است به شرح زیر است:
// CreneauMedecinJour --> نقشه
public static Map<String, Object> getMapForCreneauMedecinJour(CreneauMedecinJour créneau) {
// آیا کاری هست که انجام دهیم؟
if (créneau == null) {
return null;
}
// فرهنگ لغت <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("creneau", getMapForCreneau(créneau.getCreneau()));
hash.put("rv", getMapForRv(créneau.getRv()));
// واژهنامه را بازگردانید
return hash;
}
- خطوط ۹–۱۰: فرهنگهای واژهای که قبلاً برای انواع [Creneau] و [Rv] مورد بحث قرار گرفتهاند، استفاده میشوند؛ بنابراین اینها حاوی یک شیء [Medecin] نیستند؛
نتایج بهدستآمده به شرح زیر است:
![]() |
یا اینها اگر روز نادرست باشد:
![]() |
یا اینها اگر شمارهٔ پزشک نامعتبر باشد:
![]() |
2.12.12. URL [/getMedecinById/{id}]
URL و [/getMedecinById/{id}] توسط اعتبارسنج [RdvMedecinsController] با روش زیر پردازش میشوند:
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
public Reponse getMedecinById(@PathVariable("id") long id) {
//وضعیت برنامه
if (messages != null) {
return new Reponse(-1, messages);
}
// بازیابی پزشک
return getMedecin(id);
}
خط ۸، روش [getMedecin] به شرح زیر است:
private Reponse getMedecin(long id) {
// دریافت پزشک
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
//آیا دکتر وجود دارد؟
if (médecin == null) {
return new Reponse(2, null);
}
// باشه
return new Reponse(0, médecin);
}
نتایج بهدستآمده به شرح زیر است:
![]() |
یا اینها اگر شمارهٔ دکتر اشتباه باشد:
![]() |
2.12.13. URL [/getClientById/{id}]
URL و [/getClientById/{id}] توسط روش زیرِ کنترلکننده [RdvMedecinsController] پردازش میشوند:
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
public Reponse getClientById(@PathVariable("id") long id) {
//وضعیت برنامه
if (messages != null) {
return new Reponse(-1, messages);
}
//در حال بازیابی مشتری
return getClient(id);
}
خط ۸: روش [getClient] به شرح زیر است:
private Reponse getClient(long id) {
// بازیابی مشتری
Client client = null;
try {
client = application.getClientById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
//آیا مشتری وجود دارد؟
if (client == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, client);
}
نتایج بهدستآمده به شرح زیر است:
![]() |
یا در صورت نادرست بودن شماره مشتری:
![]() |
2.12.14. URL [/getCreneauById/{id}]
URL و [/getCreneauById/{id}] توسط روش زیر در کنترلکننده [RdvMedecinsController] پردازش میشوند:
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
public Reponse getCreneauById(@PathVariable("id") long id) {
//وضعیت درخواست
if (messages != null) {
return new Reponse(-1, messages);
}
// در حال بازیابی بازه زمانی
Reponse réponse = getCreneau(id);
if (réponse.getStatus() == 0) {
réponse.setData(Static.getMapForCreneau((Creneau) réponse.getData()));
}
// نتیجه
return réponse;
}
خط ۸: روش [getCreneau] به شرح زیر است:
private Reponse getCreneau(long id) {
// در حال بازیابی شکاف زمانی
Creneau créneau = null;
try {
créneau = application.getCreneauById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// آیا اسلات از قبل وجود دارد؟
if (créneau == null) {
return new Reponse(2, null);
}
// OK
return new Reponse(0, créneau);
}
نتایج بهدستآمده به شرح زیر است:
![]() |
یا اینها اگر شماره اسلات نادرست باشد:
![]() |
2.12.15. URL [/getRvById/{id}]
URL و [/getRvById/{id}] توسط روش زیر در کنترلکننده [RdvMedecinsController] پردازش میشوند:
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
public Reponse getRvById(@PathVariable("id") long id) {
//وضعیت درخواست
if (messages != null) {
return new Reponse(-1, messages);
}
// در حال بازیابی وقت ملاقات
Reponse réponse = getRv(id);
if (réponse.getStatus() == 0) {
réponse.setData(Static.getMapForRv2((Rv) réponse.getData()));
}
// نتیجه
return réponse;
}
خط ۸: روش [getRv] به شرح زیر است:
private Reponse getRv(long id) {
// در حال بازیابی Rv
Rv rv = null;
try {
rv = application.getRvById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
//آیا Rv وجود دارد؟
if (rv == null) {
return new Reponse(2, null);
}
// OK
return new Reponse(0, rv);
}
خط ۱۰: متد [Static.getMapForRv2] به شرح زیر است:
// RV → نقشه
public static Map<String, Object> getMapForRv2(Rv rv) {
// کاری هست انجام بدیم؟
if (rv == null) {
return null;
}
// فرهنگ لغت <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", rv.getId());
hash.put("idClient", rv.getIdClient());
hash.put("idCreneau", rv.getIdCreneau());
// واژهنامه را بازگردانید
return hash;
}
نتایج بهدستآمده به شرح زیر است:
![]() |
یا در صورت نادرست بودن شماره قرار ملاقات:
![]() |
2.12.16. URL [/ajouterRv]
URL و [/ajouterRv] توسط روش زیر در کنترلکننده [RdvMedecinsController] پردازش میشوند:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
//وضعیت برنامه
if (messages != null) {
return new Reponse(-1, messages);
}
// بازیابی مقادیر ارسالشده
String jour = post.getJour();
long idCreneau = post.getIdCreneau();
long idClient = post.getIdClient();
// تاریخ را بررسی کنید
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(6, null);
}
// بازیابی بازه زمانی
Reponse réponse = getCreneau(idCreneau);
if (réponse.getStatus() != 0) {
return réponse;
}
Creneau créneau = (Creneau) réponse.getData();
//بازیابی مشتری
réponse = getClient(idClient);
if (réponse.getStatus() != 0) {
réponse.incrStatusBy(2);
return réponse;
}
Client client = (Client) réponse.getData();
// افزودن قرار ملاقات
Rv rv = null;
try {
rv = application.ajouterRv(jourAgenda, créneau, client);
} catch (Exception e1) {
return new Reponse(5, Static.getErreursForException(e1));
}
//پاسخ را بازمیگرداند
return new Reponse(0, Static.getMapForRv(rv));
}
در اینجا چیزی نیست که قبلاً ندیده باشیم. در خط ۴۱، قرار ملاقاتی که در خط ۳۶ اضافه شده است بازگردانده میشود.
نتایج بهدستآمده با کلاینت [Advanced Rest Client] به این صورت است:
![]() |
یا مانند این اگر، برای مثال، شماره شکاف غیرموجودی را وارد کنید:
![]() |
![]() |
2.12.17. URL [/supprimerRv]
URL [/supprimerRv] توسط متد زیر از کنترلر [RdvMedecinsController] پردازش میشود:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
//وضعیت برنامه
if (messages != null) {
return new Reponse(-1, messages);
}
// ارزشهای ارسالشده را بازیابی میکنیم
long idRv = post.getIdRv();
//بازیابی مقدار بازگشتی
Reponse réponse = getRv(idRv);
if (réponse.getStatus() != 0) {
return réponse;
}
// حذف متغیر پاسخ
try {
application.supprimerRv(idRv);
} catch (Exception e1) {
return new Reponse(3, Static.getErreursForException(e1));
}
// OK
return new Reponse(0, null);
}
نتایج بهدستآمده بهشکل زیر است: follows: :
![]() |
یا اینها، اگر شماره قرار ملاقات وجود نداشته باشد:
![]() |
ما اکنون کار با کنترلر را به پایان رساندیم. اکنون به نحوه پیکربندی پروژه میپردازیم.
2.12.18. پیکربندی سرویس وب
![]() |
کلاس پیکربندی [AppConfig] به شرح زیر است:
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class })
public class AppConfig {
}
- خط ۱۰: مشخص میکند که کامپوننتهای Spring باید در پکیج [AutoConfiguration] و زیرپکیجهای آن جستجو شوند. این کامپوننتها به این صورت شناسایی خواهند شد:
- خط ۱۰: مشخص میکنیم که کامپوننتهای Spring باید در پکیج [rdvmedecins.web] و زیرپکیجهای آن جستجو شوند. به این ترتیب، کامپوننتهای زیر کشف خواهند شد:
- [@RestController RdvMedecinsController] در بسته [rdvmedecins.web.controllers]؛
- [@Component ApplicationModel] در بسته [rdvmedecins.web.models]؛
- خط ۱۱: کلاس [DomainAndPersistenceConfig] وارد میشود که پروژه [rdvmedecins-metier-dao] را برای دسترسی به بی́nهای آن پروژه پیکربندی میکند؛
2.12.19. کلاس اجرایی برای سرویس وب
![]() |
کلاس [Boot] به شرح زیر است:
package rdvmedecins.web.boot;
import org.springframework.boot.SpringApplication;
import rdvmedecins.web.config.AppConfig;
public class Boot {
public static void main(String[] args) {
SpringApplication.run(AppConfig.class, args);
}
}
در خط ۱۰، متد استاتیک [SpringApplication.run] با کلاس پیکربندی پروژه [AppConfig] به عنوان اولین پارامتر اجرا میشود. این متد پروژه را بهطور خودکار پیکربندی میکند، سرور Tomcat جاسازیشده در وابستگیها را راهاندازی میکند و کنترلر [RdvMedecinsController] را روی آن مستقر میسازد.
لاگهای تولیدشده در حین اجرا به شرح زیر است:
- خط 17: سرور Tomcat راهاندازی میشود؛
- خطوط ۲۳–۳۱: لایههای [métier, DAO, JPA] اولیه میشوند؛
- خط ۳۴: متد handle در URL [/getRvMedecinJour/{idMedecin}/{jour}] کشف شده است. این فرآیند کشف متدهای کنترلکننده تا خط ۴۴ ادامه مییابد؛
- خط ۵۲: سروِلت اسپرینگ MVC [DispatcherServlet] آماده پاسخگویی به درخواستهای کلاینتهای وب است؛
اکنون یک سرویس وب کاملاً عملیاتی داریم که از طریق یک کلاینت وب قابل دسترسی است. اکنون به تأمین امنیت این سرویس میپردازیم: میخواهیم تنها افراد خاصی بتوانند قرارهای ملاقات پزشکان را مدیریت کنند. برای این کار از چارچوب Spring Security، بخشی از اکوسیستم Spring، استفاده خواهیم کرد.
2.13. مقدمهای بر Spring Security
ما بار دیگر با دنبال کردن مراحل ۱ تا ۳ زیر، یک راهنمای Spring را وارد خواهیم کرد:
![]() |
![]() |
پروژه از عناصر زیر تشکیل شده است:
- در پوشه [templates]، صفحات پروژه HTML را مییابیم؛
- [Application]: کلاس اجرایی پروژه است؛
- [MvcConfig]: کلاس پیکربندی Spring MVC است؛
- [WebSecurityConfig]: کلاس پیکربندی Spring Security است؛
2.13.1. پیکربندی Maven
پروژه [3] یک پروژه Maven است. بیایید فایل [pom.xml] آن را بررسی کنیم تا وابستگیهایش را ببینیم:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
- خطوط ۱–۵: پروژه یک پروژه Spring Boot است؛
- خطوط ۸–۱۱: وابستگی به فریمورک [Thymeleaf]، که امکان ایجاد صفحات پویا HTML را فراهم میکند. این فریمورک میتواند جایگزین JSP (صفحات سرور جاوا)، که تا همین اواخر پیشفرض بود، با فریمورک ویوی Spring MVC شود؛
- خطوط ۱۲–۱۵: وابستگی به چارچوب Spring Security؛
2.13.2. ویوهای Thymeleaf
![]() |
ویو [home.html] به شرح زیر است:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click <a th:href="@{/hello}">here</a> to see a greeting.
</p>
</body>
</html>
- ویژگیهای [th:xx] ویژگیهای Thymeleaf هستند. این ویژگیها قبل از ارسال صفحه HTML به کلاینت، توسط Thymeleaf تفسیر میشوند. کلاینت آنها را نمیبیند؛
- خط ۱۲: ویژگی [th:href="@{/hello}"] ویژگی [href] از تگ را تولید خواهد کرد. مقدار [@{/hello}] مسیر [<context>/hello] را تولید میکند، که در آن [context] زمینهٔ برنامهٔ وب است؛
کد تولیدشده 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 به شرح زیر است:
- خط ۸: ترجمه 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 به شرح زیر است:
توجه کنید که در خط ۲۱، Thymeleaf یک فیلد مخفی به نام [_csrf] اضافه کرده است.
2.13.3. پیکربندی Spring MVC
![]() |
کلاس [MvcConfig] فریمورک Spring را پیکربندی میکند MVC:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
- خط ۷: آناوتیشن [@Configuration] کلاس [MvcConfig] را به عنوان یک کلاس پیکربندی تعیین میکند؛
- خط ۸: کلاس [MvcConfig] از کلاس [WebMvcConfigurerAdapter] ارث میبرد تا برخی متدها را بازنویسی کند؛
- خط ۱۰: بازتعریف یک متد از کلاس والد؛
- خطوط ۱۱–۱۶: متد [addViewControllers] به URL اجازه میدهد تا با ویوهای HTML مرتبط شود. ارتباطات زیر برقرار میشوند:
نما | |
/templates/home.html | |
/templates/hello.html | |
/templates/login.html |
پسوند [html] و پوشه [templates] مقادیر پیشفرض مورد استفاده توسط Thymeleaf هستند. آنها را میتوان از طریق پیکربندی تغییر داد. پوشه [templates] باید در ریشه classpath پروژه قرار داشته باشد:
![]() |
در [1]، پوشههای [main] و [resources] هر دو پوشههای منبع هستند. این بدان معناست که محتویات آنها در ریشهٔ مسیر کلاس پروژه قرار خواهند گرفت. بنابراین، در [2]، پوشههای [hello] و [templates] در ریشهٔ مسیر کلاس پروژه قرار خواهند گرفت.
2.13.4. پیکربندی Spring Security
![]() |
کلاس [WebSecurityConfig] چارچوب Spring Security را پیکربندی میکند:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
- خط ۹: آناوتیشن [@Configuration] کلاس [WebSecurityConfig] را بهعنوان یک کلاس پیکربندی تعیین میکند؛
- خط ۱۰: anotation [@EnableWebSecurity] کلاس [WebSecurityConfig] را بهعنوان یک کلاس پیکربندی Spring Security تعیین میکند؛
- خط ۱۱: کلاس [WebSecurity] از کلاس [WebSecurityConfigurerAdapter] ارث میبرد تا برخی متدها را بازنویسی کند؛
- خط ۱۲: یک متد از کلاس والد بازنویسی شده است؛
- خطوط ۱۳–۱۶: متد [configure(HttpSecurity http)] برای تعریف حقوق دسترسی به کلاسهای مختلف URL در برنامه، بازنویسی شده است؛
- خط ۱۴: متد [http.authorizeRequests()] امکان اختصاص حقوق دسترسی به URLها را فراهم میکند. در اینجا وابستگیهای زیر برقرار میشوند:
قاعده | کد | |
دسترسی بدون احراز هویت | | |
دسترسی فقط با احراز هویت |
- خط ۱۵: روش احراز هویت را تعریف میکند. احراز هویت از طریق فرمهایی که برای همه قابل دسترسی هستند انجام میشود: URL، [/login] و [http.formLogin().loginPage("/login").permitAll()]. تابع خروج نیز برای همه قابل دسترسی است.
- خطوط ۱۹–۲۱: روش [configure(AuthenticationManagerBuilder auth)] را که مدیریت کاربران را بر عهده دارد، مجدداً تعریف میکنند؛
- خط ۲۰: احراز هویت با استفاده از کاربران پیشتعریفشده [auth.inMemoryAuthentication()] انجام میشود. یک کاربر در اینجا با نام کاربری [user]، رمز عبور [password] و نقش [USER] تعریف شده است. کاربرانی با نقش یکسان میتوانند دارای حقوق یکسانی باشند؛
2.13.5. کلاس قابل اجرا
![]() |
کلاس [Application] به شرح زیر است:
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {
public static void main(String[] args) throws Throwable {
SpringApplication.run(Application.class, args);
}
}
- خط ۸: آناوتیشن [@EnableAutoConfiguration] به Spring Boot (خط ۳) دستور میدهد پیکربندیای را که توسعهدهنده به صراحت انجام نداده است، انجام دهد؛
- خط ۹: کلاس [Application] را به یک کلاس پیکربندی Spring تبدیل میکند؛
- خط ۱۰: به سیستم دستور میدهد تا دایرکتوری حاوی کلاس [Application] را برای جستجوی کامپوننتهای Spring اسکن کند. دو کلاس [MvcConfig] و [WebSecurityConfig] به این ترتیب کشف خواهند شد زیرا دارای انوتیشن [@Configuration] هستند؛
- خط ۱۳: متد [main] از کلاس قابل اجرا؛
- خط ۱۴: متد استاتیک [SpringApplication.run] با کلاس پیکربندی [Application] بهعنوان پارامتر اجرا میشود. ما قبلاً با این فرایند مواجه شدهایم و میدانیم که سرور Tomcat جاسازیشده در وابستگیهای Maven پروژه راهاندازی شده و پروژه روی آن مستقر خواهد شد. ما دیدهایم که چهار نمونه از URL توسط [/, /home, /login, /hello] مدیریت میشدند و برخی از آنها توسط حقوق دسترسی محافظت میشدند.
2.13.6. آزمایش برنامه
بیایید با درخواست URL [/] شروع کنیم که یکی از چهار نمونه پذیرفتهشده URL است. این با نمای [/templates/home.html] مرتبط است:
![]() |
URL و [/] درخواستی برای همه قابل دسترسی است. به همین دلیل ما توانستیم آن را به دست آوریم. لینک [here] به شرح زیر است:
URL و [/hello] زمانی که روی لینک کلیک کنید درخواست خواهند شد. این مورد محافظت شده است:
قانون | کد | |
دسترسی بدون احراز هویت | | |
دسترسی فقط با احراز هویت |
برای دسترسی به آن باید احراز هویت شوید. سپس Spring Security مرورگر مشتری را به صفحهٔ احراز هویت هدایت میکند. بر اساس پیکربندی نمایشدادهشده، این صفحه در URL [/login] قرار دارد. این صفحه برای همه قابل دسترسی است:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
بنابراین ما به [1] میرسیم:
![]() |
کد منبع صفحه حاصل به شرح زیر است:
- در خط ۷، یک فیلد مخفی ظاهر میشود که در صفحهٔ اصلی [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>
2.13.7. نتیجهگیری
در مثال قبلی، میتوانستیم ابتدا وباپلیکیشن را بنویسیم و سپس آن را ایمن کنیم. Spring Security غیرمداخله گر است. امکان پیادهسازی امنیت برای یک وباپلیکیشن که قبلاً نوشته شده است وجود دارد. علاوه بر این، ما به نکات زیر پی بردیم:
- امکان تعریف یک صفحه احراز هویت وجود دارد؛
- احراز هویت باید با توکن CSRF صادر شده توسط Spring Security همراه باشد؛
- اگر احراز هویت ناموفق باشد، کاربر به صفحه احراز هویت هدایت میشود، با یک پارامتر اضافی «error» در توکن URL؛
- اگر احراز هویت موفقیتآمیز باشد، شما به صفحهای که در زمان احراز هویت درخواست شده بود، هدایت میشوید. اگر صفحه احراز هویت مستقیماً بدون عبور از یک صفحه واسطه درخواست شود، Spring Security شما را به URL [/] هدایت میکند (این سناریو پوشش داده نشده است)؛
- شما با درخواست URL [/logout] با یک POST از سیستم خارج میشوید. سپس Spring Security ما را با پارامتر «logout» در URL به صفحه احراز هویت هدایت میکند؛
تمام این نتیجهگیریها بر اساس رفتار پیشفرض Spring Security است. این رفتار را میتوان از طریق پیکربندی با بازتعریف برخی متدهای کلاس [WebSecurityConfigurerAdapter] تغییر داد.
آموزش قبلی در ادامه کمک چندانی به ما نخواهد کرد. در واقع، ما از موارد زیر استفاده خواهیم کرد:
- یک پایگاه داده برای ذخیره کاربران، رمزهای عبور و نقشهای آنها؛
- احراز هویت مبتنی بر هدر (HTTP);
آموزههای نسبتاً کمی برای کاری که ما میخواهیم انجام دهیم در دسترس است. راهحلی که ما ارائه خواهیم داد، گردآوری کدهایی است که از اینجا و آنجا یافت شدهاند.
2.14. راهاندازی امنیت برای سرویس وب قرار ملاقات
2.14.1. پایگاه داده
پایگاه داده [rdvmedecins] در حال بهروزرسانی است تا کاربران، رمزهای عبور و نقشهای آنها را در خود جای دهد. سه جدول جدید اضافه شدهاند:

جدول [USERS]: کاربران
- ID: کلید اصلی؛
- VERSION: ستون نسخهبندی ردیف؛
- IDENTITY: یک شناسه توصیفی برای کاربر؛
- LOGIN: نام کاربری کاربر؛
- PASSWORD: رمز عبور آنها؛
در جدول USERS، گذرواژهها بهصورت متن ساده ذخیره نمیشوند:
![]() |
الگوریتم مورد استفاده برای رمزگذاری گذرواژهها، الگوریتم BCRYPT است.
جدول [ROLES]: نقشها
- ID: کلید اصلی؛
- VERSION: ستون نسخهبندی ردیف؛
- NAME: نام نقش. به طور پیشفرض، Spring Security انتظار نامهایی به شکل ROLE_XX را دارد، برای مثال ROLE_ADMIN یا ROLE_GUEST;
![]() |
جدول [USERS_ROLES]: جدول الحاق برای USERS / ROLES
یک کاربر ممکن است نقشهای متعددی داشته باشد و یک نقش ممکن است شامل کاربران متعددی باشد. یک رابطهٔ چندبهچند توسط جدول [USERS_ROLES] نمایش داده میشود.
- ID: کلید اصلی؛
- VERSION: ستون نسخهبرداری ردیف؛
- USER_ID: شناسه کاربر؛
- ROLE_ID: شناسه نقش؛
![]() |
از آنجایی که در حال اصلاح پایگاه داده هستیم، تمام لایهها در پروژه [métier, DAO, JPA] باید اصلاح شوند:
![]() |
2.14.2. پروژه جدید اکلیپس برای [métier, DAO, JPA]
ما پروژه اصلی [rdvmedecins-metier-dao] را به عنوان [rdvmedecins-metier-dao-v2] کپی میکنیم:
![]() |
- به [1]: پروژه جدید؛
- به [2]: تغییرات انجامشده برای رفع مسائل امنیتی در یک بسته واحد، [rdvmedecins.security]، تجمیع شدهاند. این عناصر جدید به لایههای [JPA] و [DAO] تعلق دارند، اما به منظور سادگی آنها را در یک بسته واحد گروهبندی کردهام.
2.14.3. نهادهای جدید [JPA]
![]() |
لایه JPA سه موجودیت جدید را تعریف میکند:
![]() |
کلاس [User] تصویر جدول [USERS] است:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {
private static final long serialVersionUID = 1L;
// ویژگیها
private String identity;
private String login;
private String password;
// تولیدکننده
public User() {
}
public User(String identity, String login, String password) {
this.identity = identity;
this.login = login;
this.password = password;
}
// هویت
@Override
public String toString() {
return String.format("User[%s,%s,%s]", identity, login, password);
}
// گیرندهها و تنظیمکنندهها
....
}
- خط ۹: کلاس از کلاس [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 به یک کاربر
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
// یک UserRole به یک Role ارجاع میدهد
@ManyToOne
@JoinColumn(name = "ROLE_ID")
private Role role;
// گیرندهها و تنظیمکنندهها
...
}
- خطوط ۱۵–۱۷: کلید خارجی را از جدول [USERS_ROLES] به جدول [USERS] تعریف کنید؛
- خطوط ۱۹–۲۱: تعریف کلید خارجی از جدول [USERS_ROLES] به جدول [ROLES];
2.14.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] را بدون افزودن هیچ متد جدیدی گسترش میدهد؛
2.14.5. کلاسهای مدیریت کاربر و نقش
![]() |
Spring Security نیازمند ایجاد کلاسی است که رابط [UsersDetail] زیر را پیادهسازی کند:
![]() |
این رابط در اینجا توسط کلاس [AppUserDetails] پیادهسازی شده است:
package rdvmedecins.security;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class AppUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
// ویژگیها
private User user;
private UserRepository userRepository;
// سازندهها
public AppUserDetails() {
}
public AppUserDetails(User user, UserRepository userRepository) {
this.user = user;
this.userRepository = userRepository;
}
// -------------------------رابط
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : userRepository.getRoles(user.getId())) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getLogin();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
// گیرنده و تنظیمکننده
...
}
- خط ۱۰: کلاس [AppUserDetails] رابط [UserDetails] را پیادهسازی میکند؛
- خطوط ۱۵–۱۶: این کلاس یک کاربر (خط ۱۵) و مخزنی را که جزئیات آن کاربر را فراهم میکند (خط ۱۶) در بر میگیرد؛
- خطوط 22–25: سازندهای که کلاس را با یک کاربر و مخزن او نمونه سازی میکند؛
- خطوط ۲۸–۳۵: پیادهسازی متد [getAuthorities] از رابط [UserDetails]. این متد باید یک مجموعه از عناصر از نوع [GrantedAuthority] یا یک نوع مشتقشده را ایجاد کند. در اینجا، ما از نوع مشتق [SimpleGrantedAuthority] (خط ۳۲) استفاده میکنیم که نام یکی از نقشهای کاربر را از خط ۱۵ در خود جای داده است؛
- خطوط 31–33: ما از لیست نقشهای کاربر در خط 15 عبور میکنیم تا یک لیست از عناصر از نوع [SimpleGrantedAuthority] بسازیم؛
- خطوط ۳۸–۴۰: متد [getPassword] از رابط [UserDetails] پیادهسازی میشود. رمز عبور کاربر در خط ۱۵ بازگردانده میشود؛
- خطوط ۳۸–۴۰: متد [getUserName] از رابط [UserDetails] پیادهسازی میشود. ورود کاربر از خط ۱۵ بازگردانده میشود؛
- خطوط ۴۷–۵۰: حساب کاربری کاربر هرگز منقضی نمیشود؛
- خطوط ۵۲–۵۵: حساب کاربری کاربر هرگز مسدود نمیشود؛
- خطوط ۵۷–۶۰: اعتبارنامههای کاربر هرگز منقضی نمیشوند؛
- خطوط ۶۲–۶۵: حساب کاربری کاربر همیشه فعال است؛
Spring Security همچنین وجود کلاسی را که رابط [AppUserDetailsService] را پیادهسازی کند، الزامی میداند:
![]() |
این رابط توسط کلاس زیر پیادهسازی شده است، [AppUserDetails]:
package rdvmedecins.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class AppUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
// جستجوی کاربر بر اساس نام کاربری
User user = userRepository.findUserByLogin(login);
// یافت شد؟
if (user == null) {
throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
}
// بازگرداندن جزئیات کاربر
return new AppUserDetails(user, userRepository);
}
}
- خط ۹: کلاس یک کامپوننت Spring خواهد بود و بنابراین در زمینهٔ آن در دسترس است؛
- خطوط ۱۲–۱۳: کامپوننت [UserRepository] در اینجا تزریق خواهد شد؛
- خطوط 16–25: پیادهسازی متد [loadUserByUsername] از رابط [UserDetailsService] (خط 10). پارامتر، نام کاربری کاربر است؛
- خط ۱۸: کاربر با استفاده از نام کاربریاش جستجو میشود؛
- خطوط ۲۰–۲۲: اگر کاربر پیدا نشود، یک استثنا پرتاب میشود؛
- خط 24: یک شیء [AppUserDetails] ساخته و رندر میشود. این شیء در واقع از نوع [UserDetails] (خط 16) است؛
2.14.6. آزمایشها برای لایه [DAO]
![]() |
ابتدا، ما یک کلاس قابل اجرا به نام [CreateUser] ایجاد میکنیم که قادر به ساختن یک کاربر با نقش است:
package rdvmedecins.security;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.security.Role;
import rdvmedecins.security.RoleRepository;
import rdvmedecins.security.User;
import rdvmedecins.security.UserRepository;
import rdvmedecins.security.UserRole;
import rdvmedecins.security.UserRoleRepository;
public class CreateUser {
public static void main(String[] args) {
// سینتکس: نام کاربری رمز عبور roleName
// سه پارامتر لازم است
if (args.length != 3) {
System.out.println("Syntaxe : [pg] user password role");
System.exit(0);
}
//بازیابی پارامترها
String login = args[0];
String password = args[1];
String roleName = String.format("ROLE_%s", args[2].toUpperCase());
// زمینهٔ Spring
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
UserRepository userRepository = context.getBean(UserRepository.class);
RoleRepository roleRepository = context.getBean(RoleRepository.class);
UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
//آیا نقش از قبل وجود دارد؟
Role role = roleRepository.findRoleByName(roleName);
// اگر وجود ندارد، آن را ایجاد کنید
if (role == null) {
role = roleRepository.save(new Role(roleName));
}
//آیا کاربر از قبل وجود دارد؟
User user = userRepository.findUserByLogin(login);
// اگر نه، آن را ایجاد کنید
if (user == null) {
// ما رمز عبور را با bcrypt هش میکنیم
String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
// کاربر را ذخیره کنید
user = userRepository.save(new User(login, login, crypt));
//رابطه با نقش ایجاد شد
userRoleRepository.save(new UserRole(user, role));
} else {
//کاربر از قبل وجود دارد – آیا او نقش درخواستی را دارد؟
boolean trouvé = false;
for (Role r : userRepository.getRoles(user.getId())) {
if (r.getName().equals(roleName)) {
trouvé = true;
break;
}
}
//اگر پیدا نشد، رابطه با نقش ایجاد شود
if (!trouvé) {
userRoleRepository.save(new UserRole(user, role));
}
}
//بستن زمینه Spring
context.close();
}
}
- خط ۱۷: کلاس منتظر سه آرگومان است که یک کاربر را تعریف میکنند: نام کاربری، رمز عبور و نقش؛
- خطوط ۲۵–۲۷: سه پارامتر بازیابی میشوند؛
- خط ۲۹: کانکست 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>
- خط ۴۶: کاربر در پایگاه داده ذخیره میشود؛
- خط ۴۸: همانطور که رابطه آنها را به نقششان پیوند میدهد؛
- خطوط ۵۱–۵۷: اگر ورود به سیستم از قبل وجود داشته باشد – سپس بررسی میکنیم که آیا نقشی که میخواهیم به آنها اختصاص دهیم، از قبل در میان نقشهایشان وجود دارد یا خیر؛
- خطوط ۵۹–۶۱: اگر نقش مورد جستجو یافت نشود، یک سطر در جدول [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] دارد؛
تستها با لاگهای زیر با موفقیت اجرا شدند:
2.14.7. نتیجهگیری موقت
کلاسهای لازم برای Spring Security با کمترین تغییرات در پروژه اصلی اضافه شدند. برای خلاصه:
- افزودن وابستگی به Spring Security در فایل [pom.xml];
- ایجاد سه جدول اضافی در پایگاه داده؛
- ایجاد اشیاء JPA و کامپوننتهای Spring در پکیج [rdvmedecins.security]؛
این سناریوی بسیار مطلوب ناشی از این واقعیت است که سه جدول افزودهشده به پایگاه داده، مستقل از جدولهای موجود هستند. ما حتی میتوانستیم آنها را در یک پایگاه داده جداگانه قرار دهیم. این امر ممکن بود زیرا ما تصمیم گرفتیم که یک کاربر به طور مستقل از پزشکان و مشتریان وجود دارد. اگر مورد دوم (پزشکان) کاربران بالقوه بودند، لازم بود که لینکهایی بین جدول [USERS] و جدولهای [MEDECINS] و [CLIENTS] ایجاد شود. این امر سپس تأثیر قابل توجهی بر پروژه موجود میگذاشت.
2.14.8. پروژه اکلیپس برای لایه [web]
![]() |
پروژه قبلی [rdvmedecins-webapi] در پروژههای [rdvmedecins-webapi-v2] و [1] تکرار شده است:
![]() |
تنها تغییراتی که باید اعمال شود در بسته [rdvmedecins.web.config] است، جایی که Spring Security باید پیکربندی شود. ما قبلاً با یک کلاس پیکربندی Spring Security مواجه شدهایم:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
ما همان رویه را دنبال خواهیم کرد:
- خط ۱۱: تعریف یک کلاس که از کلاس [WebSecurityConfigurerAdapter] ارث میبرد؛
- خط ۱۳: تعریف یک متد [configure(HttpSecurity http)] که حقوق دسترسی به کلاسهای مختلف URL در سرویس وب را مشخص میکند؛
- خط ۱۹: متد [configure(AuthenticationManagerBuilder auth)] را تعریف کنید که کاربران و نقشهای آنها را مشخص میکند؛
امنیت اسپرینگ از طریق کلاس [SecurityConfig] پیکربندی میشود:
package rdvmedecins.web.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import rdvmedecins.security.AppUserDetailsService;
@EnableAutoConfiguration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AppUserDetailsService appUserDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder registry) throws Exception {
//احراز هویت توسط bean [appUserDetailsService] انجام میشود
// رمز عبور با استفاده از الگوریتم هش Bcrypt رمزگذاری میشود
registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//CSRF
http.csrf().disable();
// رمز عبور از طریق هدر Authorisation: Basic xxxx ارسال میشود
http.httpBasic();
// فقط نقش ADMIN میتواند از برنامه استفاده کند
http.authorizeRequests() //
.antMatchers("/", "/**") // تمام URL
.hasRole("ADMIN");
}
}
- خطوط ۱۴–۱۵: anotationها (علامتگذاریها) از مثال استفاده شدهاند؛
- خطوط 17–18: کلاس [AppUserDetails] که دسترسی به کاربران برنامه را فراهم میکند، تزریق شده است؛
- خطوط ۲۰–۲۱: متد [configure(HttpSecurity http)] کاربران و نقشهای آنها را تعریف میکند. این متد یک پارامتر از نوع [AuthenticationManagerBuilder] میگیرد. این پارامتر با دو مورد اطلاعات غنیسازی میشود:
- یک مرجع به سرویس [appUserDetailsService] در خط ۱۸، که دسترسی را به کاربران ثبتنامشده اعطا میکند. در اینجا باید توجه داشت که هیچ نشانهای وجود ندارد که آنها در یک پایگاه داده ذخیره شده باشند. بنابراین، آنها میتوانند در یک کش (cache) نگهداری شوند، توسط یک سرویس وب بازگردانده شوند و غیره.
- نوع رمزنگاری است که برای رمز عبور استفاده میشود. در اینجا باید توجه داشت که ما از الگوریتم BCrypt استفاده کردهایم؛
- خطوط 27–40: متد [configure(HttpSecurity http)] حقوق دسترسی به توکنهای URL سرویس وب را تعریف میکند؛
- خط ۳۰: همانطور که در پروژه مقدماتی دیدیم، Spring Security به طور پیشفرض یک توکن CSRF (فریب درخواست بینسایتی) را مدیریت میکند که کاربر برای احراز هویت باید آن را به سرور بازگرداند. در اینجا، این مکانیزم غیرفعال شده است؛
- خط ۳۲: ما احراز هویت را از طریق هدر HTTP فعال میکنیم. کلاینت باید هدر زیر را ارسال کند:
که در آن «code» رمزگذاری Base64 رشته «login:password» است. به عنوان مثال، رمزگذاری Base64 رشته «admin:admin» برابر است با «YWRtaW46YWRtaW4=». بنابراین، کاربری با نام کاربری [admin] و رمز عبور [admin]، برای احراز هویت، سربرگ زیر را ارسال خواهد کرد: HTTP
- خطوط ۳۴–۳۶: نشان میدهند که تمام منابع URL سرویس وب برای کاربرانی با نقش [ROLE_ADMIN] قابل دسترسی هستند. این بدان معناست که کاربری بدون این نقش نمیتواند به سرویس وب دسترسی پیدا کند؛
کلاس [AppConfig] که کل برنامه را پیکربندی میکند، به شرح زیر تغییر مییابد:
![]() |
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class })
public class AppConfig {
}
- تغییر در خط ۱۱ انجام شده است: مشخص شده است که اکنون دو فایل پیکربندی برای استفاده وجود دارند: [DomainAndPersistenceConfig] و [SecurityConfig].
2.14.9. آزمون سرویس وب
ما سرویس وب را با استفاده از کلاینت کروم [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 را تولید کنیم، سرویس وب امن را راهاندازی میکنیم. سپس با استفاده از کلاینت کروم [Advanced Rest Client]، فهرستی از همه پزشکان را درخواست میکنیم:
![]() |
- در [1]، ما URL را برای پزشکان درخواست میکنیم؛
- در [2]، با استفاده از متد GET؛
- در [3]، هدر احراز هویت HTTP را ارائه میدهیم. کد [YWRtaW46YWRtaW4=] رمزگذاری Base64 رشته [admin:admin] است؛
- در [4]، ما فرمان HTTP را ارسال میکنیم؛
پاسخ سرور به شرح زیر است:
![]() |
- در [1]، هدر احراز هویت HTTP;
- در [2]، سرور پاسخ JSON را بازمیگرداند؛
- در [3]، فهرست پزشکان.
حال بیایید یک درخواست HTTP را با یک هدر احراز هویت نادرست امتحان کنیم. پاسخ سپس به شرح زیر است:
![]() |
- در [1] و [3]: هدر احراز هویت HTTP;
- به [2]: پاسخ سرویس وب؛
اکنون، بیایید کاربر 'user' / 'user' را امتحان کنیم. این کاربر وجود دارد اما به سرویس وب دسترسی ندارد. اگر برنامه رمزگذاری Base64 را با دو آرگومان [user user] اجرا کنیم:
![]() |
نتیجه زیر را دریافت میکنیم:
![]() |
- در [1] و [3]: هدر احراز هویت HTTP;
- به [2]: پاسخ سرویس وب. این با مورد قبلی که [401 Unauthorized] بود، متفاوت است. در این مورد، کاربر با موفقیت احراز هویت شده اما حقوق کافی برای دسترسی به URL را ندارد؛
2.15. Conclusion
بیایید معماری کلی برنامهٔ کلاینت/سرور خود را به یاد بیاوریم:
![]() |
یک سرویس وب امن اکنون راهاندازی شده و در حال اجرا است. خواهیم دید که به دلیل مشکلاتی که در حین توسعه کلاینت Angular با شناسه JS به وجود میآید، لازم است در آن تغییراتی ایجاد شود. با این حال، تا زمان مواجهه با مشکل، برای حل آن صبر خواهیم کرد. اکنون کلاینت Angular را میسازیم که یک رابط وب برای مدیریت قرار ملاقاتهای پزشکان فراهم خواهد کرد.

















































































































































