دليل Spring Boot: بناء تطبيقات Java سريعة وعصرية خطوة بخطوة
مقدمة: لماذا يُعد Spring Boot خياراً ممتازاً لبناء تطبيقات Java الحديثة؟
يُستخدم Spring Boot على نطاق واسع في تطوير تطبيقات Java الحديثة، لأنه يختصر كثيراً من الإعدادات التقليدية ويمنح المطور بيئة عمل سريعة ومرنة. في هذا الدليل العملي سنبني نموذجاً أولياً لتطبيق حجز مرافق، مناسباً لفهم آلية العمل في المشاريع الواقعية، خصوصاً عندما تحتاج إلى إطلاق نسخة أولية بسرعة دون التضحية بجودة البنية التقنية.
الفكرة هنا ليست بناء نظام مثالي منذ اليوم الأول، بل إنشاء تطبيق عملي قابل للتطوير لاحقاً. وهذا الأسلوب مهم جداً في مشاريع الشركات الناشئة، والنماذج التجريبية، وحتى أنظمة الإدارة الداخلية.

فكرة المشروع: نظام حجز مرافق سكنية
سننشئ تطبيقاً لإدارة حجز المرافق داخل مجمع سكني أو منشأة مشابهة. يستطيع المستخدم تسجيل الدخول ثم حجز موعد لاستخدام مرفق مثل:
- Fitness Center
- Pool
- Sauna
وستكون لكل خدمة سعة محددة Capacity لضمان عدم تجاوز عدد المستخدمين المسموح به في نفس الوقت.
الميزات الأساسية في التطبيق
- تسجيل دخول المستخدمين فقط دون صفحة Sign-up.
- عرض الحجوزات الخاصة بكل مستخدم.
- إنشاء حجز جديد بتحديد نوع المرفق والتاريخ ووقت البداية والنهاية.
- حماية صفحات الحجز بحيث لا يصل إليها إلا المستخدمون المسجلون.
- التحقق من السعة المتاحة قبل قبول أي حجز جديد.
المتطلبات المسبقة قبل البدء
حتى تستفيد من هذا الشرح عملياً، يُفضل أن تكون لديك معرفة أساسية بـ:
- لغة Java ومبادئ OOP.
- قواعد البيانات العلائقية والعلاقات مثل one-to-many و many-to-many.
- مبادئ أولية في Spring.
- أساسيات HTML.
كما ستحتاج إلى الأدوات التالية:
- أحدث إصدار من JDK.
- بيئة تطوير مثل IntelliJ IDEA أو أي Java IDE مشابه.
التقنيات التي سنستخدمها في المشروع
هذا التطبيق يمر على مجموعة مهمة من الأدوات التي يحتاجها مطور Spring Boot في المشاريع الحديثة:
- Bootify
- Hibernate
- Spring Boot
- Maven
- JPA
- Swagger
- H2 In-Memory Database
- Thymeleaf
- Bootstrap
- Spring Security
لماذا Spring Boot مناسب للنماذج الأولية السريعة؟
رغم أن إطار Spring يرتبط غالباً بالمشاريع المؤسسية الكبيرة، فإنه قوي أيضاً في بناء النماذج الأولية بسرعة. يعود ذلك إلى عدة أسباب:
- الاعتماد المكثف على Annotations لتوليد كثير من السلوكيات خلف الكواليس.
- سهولة الدمج مع مكتبات مثل Lombok لتقليل الشيفرة المتكررة.
- دعم ممتاز لقواعد البيانات المؤقتة مثل H2 دون الحاجة إلى إعداد قاعدة بيانات فعلية منذ البداية.
- بيئة ناضجة وغنية بالمكتبات والوثائق والحلول.
- تقليل الإعدادات اليدوية إلى الحد الأدنى بفضل Spring Boot.
- وجود Spring Security كحل متقدم ومجرب لإدارة الأمان.
إنشاء المشروع باستخدام Bootify
لبداية أسرع، سنستخدم أداة Bootify التي تولّد جزءاً كبيراً من الشيفرة الأساسية تلقائياً، ما يساعدك على التركيز على منطق العمل بدلاً من كتابة الملفات الأولية يدوياً.
الإعدادات المقترحة عند إنشاء المشروع
- Build Type: Maven
- Java Version: 14
- تفعيل Lombok
- DBMS: H2 database
- إضافة dateCreated/lastUpdated إلى الكيانات
- Packages: Technical
- تفعيل OpenAPI/Swagger UI
- إضافة org.springframework.boot:spring-boot-devtools إلى Dependencies

تصميم الكيانات الأساسية في التطبيق
سنحتاج إلى ثلاث لبنات منطقية رئيسية:
- Reservation: يمثل بيانات الحجز.
- User: يمثل المستخدم.
- AmenityType: يمثل نوع المرفق المحجوز.
بدلاً من إنشاء كيان مستقل باسم Amenity مع علاقة معقدة، سنستخدم enum لتخزين أنواع المرافق داخل كيان الحجز مباشرة. هذا القرار مناسب للنظام البسيط ويخفف التعقيد غير الضروري.


العلاقة بين User و Reservation
العلاقة هنا من نوع Many-to-One، لأن المستخدم الواحد يمكن أن يمتلك عدة حجوزات، بينما كل حجز يعود إلى مستخدم واحد فقط.



بنية المشروع بعد التوليد
بعد تنزيل المشروع وفتحه في IntelliJ IDEA، ستجد هيكل ملفات قريباً من التالي:
├── amenity-reservation-system.iml
├── mvnw
├── mvnw.cmd
├── pom.xml
├── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── amenity_reservation_system
│ │ ├── AmenityReservationSystemApplication.java
│ │ ├── HomeController.java
│ │ ├── config
│ │ │ ├── DomainConfig.java
│ │ │ ├── JacksonConfig.java
│ │ │ └── RestExceptionHandler.java
│ │ ├── domain
│ │ │ ├── Reservation.java
│ │ │ └── User.java
│ │ ├── model
│ │ │ ├── ErrorResponse.java
│ │ │ ├── FieldError.java
│ │ │ ├── ReservationDTO.java
│ │ │ └── UserDTO.java
│ │ ├── repos
│ │ │ ├── ReservationRepository.java
│ │ │ └── UserRepository.java
│ │ ├── rest
│ │ │ ├── ReservationController.java
│ │ │ └── UserController.java
│ │ └── service
│ │ ├── ReservationService.java
│ │ └── UserService.java
│ └── resources
│ └── application.yml
└── target
فهم الطبقات المولدة في مشروع Spring Boot
طبقة Repositories
مجلد repos يحتوي على طبقة الوصول إلى البيانات. هنا نستفيد من JPA لإنشاء استعلامات جاهزة عبر أسماء الدوال فقط، من دون كتابة SQL صريح في حالات كثيرة.

طبقة Models والكيانات
الكيانات المعرّفة بـ @Entity تتحول إلى جداول داخل قاعدة البيانات بواسطة Hibernate. كما يمكن تمثيل العلاقات بين الجداول مباشرة داخل الكود.
@OneToMany(mappedBy = "user")
private Set<Reservation> userReservations;
وفي الجهة المقابلة داخل Reservation:
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
طبقة Controllers
المتحكمات Controllers تستقبل الطلبات وتعيد استجابات مناسبة، سواء كانت JSON أو صفحات HTML.
طبقة Services
يُفضل أن يبقى Controller خفيفاً، بينما يُوضع منطق الأعمال داخل Service. هذا النمط يجعل المشروع أوضح وأسهل في الاختبار والتوسع.
تجربة Swagger واستكشاف الـ API
بعد تشغيل التطبيق، يمكنك زيارة الرابط التالي لعرض واجهة Swagger UI:
http://localhost:8080/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config#/

يمكنك مثلاً إنشاء مستخدم عبر POST من خلال UserController.


إعداد application.yml لدعم H2 Console
حتى تتمكن من فحص البيانات داخل H2، أضف الإعدادات التالية إلى ملف application.yml:
spring:
datasource:
url: ${ JDBC_DATABASE_URL :jdbc:h2:mem:amenity-reservation-system}
username : ${ JDBC_DATABASE_USERNAME :sa}
password : ${ JDBC_DATABASE_PASSWORD :}
dbcp2 :
max-wait-millis: 30000
validation-query: "SELECT 1"
validation-query-timeout: 30
jpa :
hibernate:
ddl-auto: update
open- in -view: false
properties :
hibernate:
jdbc:
lob:
non_contextual_creation: true
id :
new_generator_mappings: true
springdoc :
pathsToMatch: /api/**
ثم افتح لوحة الإدارة:
http://localhost:8080/h2-console/



تعديل الشيفرة لتناسب التطبيق الفعلي
الشيفرة المولدة مفيدة كنقطة انطلاق، لكنها لا تطابق احتياجاتنا بالكامل. لذلك سنزيل بعض الأجزاء غير الضرورية مثل DTOs وREST controllers التي لا نحتاجها في تطبيق يعتمد على الواجهات.
cd src/main/java/com/amenity_reservation_system/
rm -rf model
rm -rf rest
rm -rf config
mv domain model
بعد ذلك يجب تعديل الخدمات لتعمل مباشرة مع الكيانات بدلاً من DTOs.
تحديث UserService
package com.amenity_reservation_system.service;
import com.amenity_reservation_system.domain.User;
import com.amenity_reservation_system.repos.UserRepository;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(final UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> findAll() {
return userRepository.findAll();
}
public User get(final Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}
public Long create(final User user) {
return userRepository.save(user).getId();
}
public void update(final Long id, final User user) {
final User existingUser = userRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
userRepository.save(user);
}
public void delete(final Long id) {
userRepository.deleteById(id);
}
}
تحديث ReservationService
package com.amenity_reservation_system.service;
import com.amenity_reservation_system.domain.Reservation;
import com.amenity_reservation_system.domain.User;
import com.amenity_reservation_system.repos.ReservationRepository;
import com.amenity_reservation_system.repos.UserRepository;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
@Service
public class ReservationService {
private final ReservationRepository reservationRepository;
private final UserRepository userRepository;
public ReservationService(final ReservationRepository reservationRepository, final UserRepository userRepository) {
this.reservationRepository = reservationRepository;
this.userRepository = userRepository;
}
public List<Reservation> findAll() {
return reservationRepository.findAll();
}
public Reservation get(final Long id) {
return reservationRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}
public Long create(final Reservation reservation) {
return reservationRepository.save(reservation).getId();
}
public void update(final Long id, final Reservation reservation) {
final Reservation existingReservation = reservationRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
reservationRepository.save(reservation);
}
public void delete(final Long id) {
reservationRepository.deleteById(id);
}
}
إضافة Thymeleaf وبناء الواجهات
سنستخدم Thymeleaf لإنشاء صفحات HTML مرتبطة ببيانات التطبيق. أضف dependency الخاصة بها في pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
ثم نفذ:
mvn clean install
إنشاء الصفحة الرئيسية
cd ../../../resources
mkdir templates
cd templates
touch index.html
<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Amenities Reservation App</title>
<link th:rel="stylesheet" th:href="@{/webjars/bootstrap/4.0.0-2/css/bootstrap.min.css}" />
</head>
<body>
<div>hello world!</div>
<script th:src="@{/webjars/jquery/3.0.0/jquery.min.js}"></script>
<script th:src="@{/webjars/popper.js/1.12.9-1/umd/popper.min.js}"></script>
<script th:src="@{/webjars/bootstrap/4.0.0-2/js/bootstrap.min.js}"></script>
</body>
</html>
إنشاء HomeController
cd ../java/com/amenity_reservation_system
mkdir controller && cd controller
touch HomeController
package com.amenity_reservation_system.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HomeController {
@GetMapping("/")
public String index(Model model) {
return "index";
}
}
تعريف AmenityType باستخدام enum
حتى نحدد نوع المرفق بطريقة بسيطة وقابلة للقراءة، سننشئ enum باسم AmenityType:
cd ../model
touch AmenityType.java
public enum AmenityType {
POOL("POOL"),
SAUNA("SAUNA"),
GYM("GYM");
private final String name;
private AmenityType(String value) {
name = value;
}
@Override
public String toString() {
return name;
}
}
ثم نضيف الحقل إلى Reservation:
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private AmenityType amenityType;
إدخال بيانات تجريبية عند تشغيل التطبيق
لتجربة النظام سريعاً، يمكننا استخدام CommandLineRunner لإضافة مستخدم وحجز تلقائياً عند بدء التشغيل.
package com.amenity_reservation_system;
import com.amenity_reservation_system.model.AmenityType;
import com.amenity_reservation_system.model.Reservation;
import com.amenity_reservation_system.model.User;
import com.amenity_reservation_system.repos.ReservationRepository;
import com.amenity_reservation_system.repos.UserRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Date;
@SpringBootApplication
public class AmenityReservationSystemApplication {
public static void main(String[] args) {
SpringApplication.run(AmenityReservationSystemApplication.class, args);
}
@Bean
public CommandLineRunner loadData(UserRepository userRepository, ReservationRepository reservationRepository) {
return (args) -> {
User user = userRepository.save(new User());
DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
Date date = new Date();
LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
Reservation reservation = Reservation.builder()
.reservationDate(localDate)
.startTime(LocalTime.of(12, 00))
.endTime(LocalTime.of(13, 00))
.user(user)
.amenityType(AmenityType.POOL)
.build();
reservationRepository.save(reservation);
};
}
}

بناء واجهة عرض الحجوزات باستخدام Thymeleaf
سنبدأ بإنشاء شريط تنقل navbar عبر Thymeleaf fragments حتى يكون قابلاً لإعادة الاستخدام.
mkdir fragments
touch nav.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
<nav th:fragment="nav" class="navbar navbar-expand navbar-dark bg-primary">
<div class="navbar-nav w-100">
<a class="navbar-brand text-color" href="/">Amenities Reservation System</a>
</div>
</nav>
</body>
</html>
بعدها يمكن إنشاء صفحة رئيسية أجمل، مع دعوة المستخدم للانتقال إلى صفحة الحجوزات.
<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Amenities Reservation App</title>
<link th:rel="stylesheet" th:href="@{/webjars/bootstrap/4.0.0-2/css/bootstrap.min.css}" />
</head>
<body>
<div>
<div th:insert="fragments/nav :: nav"></div>
<div class="text-light" style="background-image: url('https://source.unsplash.com/1920x1080/?nature'); position: absolute; left: 0; top: 0; opacity: 0.6; z-index: -1; min-height: 100vh; min-width: 100vw;"></div>
<div class="container" style="padding-top: 20vh; display: flex; flex-direction: column; align-items: center;">
<h1 class="display-3">Reservation management made easy.</h1>
<p class="lead">واجهة بسيطة تمهّد للمستخدم الوصول إلى نظام الحجز بسرعة ووضوح.</p>
<a href="/reservations" class="btn btn-success btn-lg my-2">Reserve an Amenity</a>
</div>
</div>
</body>
</html>

عرض حجوزات المستخدم
الآن نضيف مساراً جديداً في HomeController لاسترجاع المستخدم وتمريره إلى الواجهة:
package com.amenity_reservation_system;
import com.amenity_reservation_system.domain.User;
import com.amenity_reservation_system.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HomeController {
final UserService userService;
public HomeController(UserService userService) {
this.userService = userService;
}
@GetMapping("/")
public String index(Model model) {
return "index";
}
@GetMapping("/reservations")
public String reservations(Model model) {
User user = userService.get(10000L);
model.addAttribute("user", user);
return "reservations";
}
}
ثم أنشئ ملف reservations.html:
touch reservations.html
<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Reservations</title>
<link th:rel="stylesheet" th:href="@{/webjars/bootstrap/4.0.0-2/css/bootstrap.min.css}" />
</head>
<body>
<div>
<div th:insert="fragments/nav :: nav"></div>
<div class="container" style="padding-top: 10vh; display: flex; flex-direction: column; align-items: center;">
<h3>Welcome <span th:text="${user.getFullName()}"></span></h3>
<br>
<table class="table">
<thead>
<tr>
<th scope="col">Amenity</th>
<th scope="col">Date</th>
<th scope="col">Start Time</th>
<th scope="col">End Time</th>
</tr>
</thead>
<tbody>
<tr th:each="reservation : ${user.getReservations()}">
<td th:text="${reservation.getAmenityType()}"></td>
<td th:text="${reservation.getReservationDate()}"></td>
<td th:text="${reservation.getStartTime()}"></td>
<td th:text="${reservation.getEndTime()}"></td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>
إذا واجهت مشكلة lazy loading، عدّل العلاقة داخل User.java إلى:
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private Set<Reservation> reservations = new HashSet<>();

إنشاء حجز جديد من الواجهة
لكي نسمح للمستخدم بإنشاء حجز جديد، نحتاج أولاً إلى ضبط تنسيقات التاريخ والوقت داخل Reservation.java:
@DateTimeFormat(pattern = "yyyy-MM-dd")
@Column(nullable = false)
private LocalDate reservationDate;
@DateTimeFormat(pattern = "HH:mm")
@Column
private LocalTime startTime;
@DateTimeFormat(pattern = "HH:mm")
@Column
private LocalTime endTime;
ثم نحدّث المتحكم ليضع كائن Reservation داخل Model ويحفظ المستخدم داخل Session:
@GetMapping("/reservations")
public String reservations(Model model, HttpSession session) {
User user = userService.get(10000L);
session.setAttribute("user", user);
Reservation reservation = new Reservation();
model.addAttribute("reservation", reservation);
return "reservations";
}
بعدها أضف زر فتح نافذة الحجز داخل reservations.html:
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#createReservationModal">
Create Reservation
</button>
<div th:insert="fragments/modal :: modal" th:with="reservation=${reservation}"></div>
قالب modal للحجز
pwd
/src/main/resources
cd templates/fragments
touch modal.html
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
<div class="modal fade" th:fragment="modal" id="createReservationModal" tabindex="-1" role="dialog" aria-labelledby="createReservationModalTitle" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createReservationModalTitle">Create Reservation</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form action="#" th:action="@{/reservations-submit}" th:object="${reservation}" method="post">
<div class="form-group row">
<label for="type-select" class="col-2 col-form-label">Amenity</label>
<div class="col-10">
<select class="form-control" id="type-select" th:field="*{amenityType}">
<option value="POOL">POOL</option>
<option value="SAUNA">SAUNA</option>
<option value="GYM">GYM</option>
</select>
</div>
</div>
<div class="form-group row">
<label for="start-date" class="col-2 col-form-label">Date</label>
<div class="col-10">
<input class="form-control" type="date" id="start-date" name="trip-start" th:field="*{reservationDate}" value="2018-07-22" min="2021-05-01" max="2021-12-31" />
</div>
</div>
<div class="form-group row">
<label for="start-time" class="col-2 col-form-label">From</label>
<div class="col-10">
<input class="form-control" type="time" id="start-time" name="time" th:field="*{startTime}" min="08:00" max="19:30" required />
</div>
</div>
<div class="form-group row">
<label for="end-time" class="col-2 col-form-label">To</label>
<div class="col-10">
<input class="form-control" type="time" id="end-time" name="time" th:field="*{endTime}" min="08:30" max="20:00" required />
<small>Amenities are available from 8 am to 8 pm</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary" value="Submit">Save changes</button>
</div>
</form>
</div>
</div>
</div>
</div>
</body>
</html>
استقبال النموذج في Controller
@PostMapping("/reservations-submit")
public String reservationsSubmit(@ModelAttribute Reservation reservation, @SessionAttribute("user") User user) {
assert user != null;
reservation.setUser(user);
reservationService.create(reservation);
Set<Reservation> userReservations = user.getReservations();
userReservations.add(reservation);
user.setReservations(userReservations);
userService.update(user.getId(), user);
return "redirect:/reservations";
}


إضافة Authentication و Authorization باستخدام Spring Security
لمنع المستخدمين من رؤية حجوزات بعضهم البعض، سنعتمد على Spring Security. أضف dependencies التالية إلى pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
ملف WebSecurityConfig
cd /src/main/java/com/amenity_reservation_system
mkdir config && cd config
touch WebSecurityConfig.java
package com.amenity_reservation_system.config;
import com.amenity_reservation_system.service.UserDetailsServiceImpl;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsServiceImpl userDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public WebSecurityConfig(UserDetailsServiceImpl userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
this.userDetailsService = userDetailsService;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/webjars/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.logout()
.permitAll()
.logoutSuccessUrl("/");
}
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
}
توسيع كيان User ليشمل بيانات الدخول
package com.amenity_reservation_system.model;
import java.time.OffsetDateTime;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class User {
@Id
@Column(nullable = false, updatable = false)
@SequenceGenerator(name = "primary_sequence", sequenceName = "primary_sequence", allocationSize = 1, initialValue = 10000)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "primary_sequence")
private Long id;
@Column(nullable = false, unique = true)
private String fullName;
@Column(nullable = false, unique = true)
private String username;
@Column
private String passwordHash;
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private Set<Reservation> reservations = new HashSet<>();
@Column(nullable = false, updatable = false)
private OffsetDateTime dateCreated;
@Column(nullable = false)
private OffsetDateTime lastUpdated;
@PrePersist
public void prePersist() {
dateCreated = OffsetDateTime.now();
lastUpdated = dateCreated;
}
@PreUpdate
public void preUpdate() {
lastUpdated = OffsetDateTime.now();
}
public User(String fullName, String username, String passwordHash) {
this.fullName = fullName;
this.username = username;
this.passwordHash = passwordHash;
}
}
إنشاء UserDetailsServiceImpl
cd service
touch UserDetailsServiceImpl.java
package com.amenity_reservation_system.service;
import com.amenity_reservation_system.model.User;
import com.amenity_reservation_system.repos.UserRepository;
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 UserDetailsServiceImpl implements UserDetailsService {
private UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
final User user = userRepository.findUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
UserDetails userDetails = org.springframework.security.core.userdetails.User.withUsername(
user.getUsername()).password(user.getPwHash()).roles("USER").build();
return userDetails;
}
}
إضافة دالة JPA في UserRepository
package com.amenity_reservation_system.repos;
import com.amenity_reservation_system.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
User findUserByUsername(String username);
}
إضافة BCryptPasswordEncoder وبيانات دخول أولية
package com.amenity_reservation_system;
import com.amenity_reservation_system.model.AmenityType;
import com.amenity_reservation_system.model.Reservation;
import com.amenity_reservation_system.model.User;
import com.amenity_reservation_system.repos.ReservationRepository;
import com.amenity_reservation_system.repos.UserRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Date;
@SpringBootApplication
public class AmenityReservationSystemApplication {
public static void main(String[] args) {
SpringApplication.run(AmenityReservationSystemApplication.class, args);
}
@Bean
public CommandLineRunner loadData(UserRepository userRepository, ReservationRepository reservationRepository) {
return (args) -> {
User user = userRepository.save(new User("Yigit Kemal Erinc", "yigiterinc", bCryptPasswordEncoder().encode("12345")));
DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
Date date = new Date();
LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
Reservation reservation = Reservation.builder()
.reservationDate(localDate)
.startTime(LocalTime.of(12, 00))
.endTime(LocalTime.of(13, 00))
.user(user)
.amenityType(AmenityType.POOL)
.build();
reservationRepository.save(reservation);
};
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
إظهار أزرار Log in و Log out في nav.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.w3.org/1999/xhtml">
<body>
<nav th:fragment="nav" class="navbar navbar-expand navbar-dark bg-primary">
<div class="navbar-nav w-100">
<a class="navbar-brand text-color" href="/">Amenities Reservation System</a>
</div>
<a sec:authorize="isAnonymous()" class="navbar-brand text-color" th:href="@{/login}">Log in</a>
<a sec:authorize="isAuthenticated()" class="navbar-brand text-color" th:href="@{/logout}">Log out</a>
</nav>
</body>
</html>

عرض حجوزات المستخدم المسجل فعلياً
بدلاً من الاعتماد على مستخدم ثابت، يمكننا جلب المستخدم الحالي من SecurityContextHolder:
@GetMapping("/reservations")
public String reservations(Model model, HttpSession session) {
UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String name = principal.getUsername();
User user = userService.getUserByUsername(name);
if (user != null) {
session.setAttribute("user", user);
Reservation reservation = new Reservation();
model.addAttribute("reservation", reservation);
return "reservations";
}
return "index";
}
وأضف هذه الدالة إلى UserService:
public User getUserByUsername(String username) {
return userRepository.findUserByUsername(username);
}
إدارة السعة Capacity ومنع الحجز الزائد
حتى يصبح النظام أكثر واقعية، يجب تعريف سعة كل مرفق والتحقق منها قبل قبول الحجز.
إنشاء كيان Capacity
package com.amenity_reservation_system.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Capacity {
@Id
@Column(nullable = false, updatable = false)
@SequenceGenerator(name = "primary_sequence", sequenceName = "primary_sequence", allocationSize = 1, initialValue = 10000)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "primary_sequence")
private Long id;
@Column(nullable = false, unique = true)
@Enumerated(EnumType.STRING)
private AmenityType amenityType;
@Column(nullable = false)
private int capacity;
public Capacity(AmenityType amenityType, int capacity) {
this.amenityType = amenityType;
this.capacity = capacity;
}
}
إنشاء CapacityRepository
package com.amenity_reservation_system.repos;
import com.amenity_reservation_system.model.Capacity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CapacityRepository extends JpaRepository<Capacity, Long> {
}
تحميل السعات الابتدائية
package com.amenity_reservation_system;
import com.amenity_reservation_system.model.AmenityType;
import com.amenity_reservation_system.model.Capacity;
import com.amenity_reservation_system.model.Reservation;
import com.amenity_reservation_system.model.User;
import com.amenity_reservation_system.repos.CapacityRepository;
import com.amenity_reservation_system.repos.ReservationRepository;
import com.amenity_reservation_system.repos.UserRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@SpringBootApplication
public class AmenityReservationSystemApplication {
private Map<AmenityType, Integer> initialCapacities = new HashMap<>() {{
put(AmenityType.GYM, 20);
put(AmenityType.POOL, 4);
put(AmenityType.SAUNA, 1);
}};
public static void main(String[] args) {
SpringApplication.run(AmenityReservationSystemApplication.class, args);
}
@Bean
public CommandLineRunner loadData(UserRepository userRepository, CapacityRepository capacityRepository) {
return (args) -> {
userRepository.save(new User("Yigit Kemal Erinc", "yigiterinc", bCryptPasswordEncoder().encode("12345")));
for (AmenityType amenityType : initialCapacities.keySet()) {
capacityRepository.save(new Capacity(amenityType, initialCapacities.get(amenityType)));
}
};
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
استعلام JPA لحساب الحجوزات المتداخلة
الفكرة هي إحصاء الحجوزات التي تقع في اليوم نفسه وتتقاطع زمنياً مع الحجز المطلوب. لهذا نستخدم استعلاماً مشتقاً باسم طويل لكنه قوي:
List<Reservation> findReservationsByReservationDateAndStartTimeBeforeAndEndTimeAfterOrStartTimeBetween(
LocalDate reservationDate,
LocalTime startTime,
LocalTime endTime,
LocalTime betweenStart,
LocalTime betweenEnd
);
التحقق من السعة داخل ReservationService
public Long create(final Reservation reservation) {
int capacity = capacityRepository.findByAmenityType(reservation.getAmenityType()).getCapacity();
int overlappingReservations = reservationRepository
.findReservationsByReservationDateAndStartTimeBeforeAndEndTimeAfterOrStartTimeBetween(
reservation.getReservationDate(),
reservation.getStartTime(),
reservation.getEndTime(),
reservation.getStartTime(),
reservation.getEndTime()).size();
if (overlappingReservations >= capacity) {
throw new CapacityFullException("This amenity's capacity is full at desired time");
}
return reservationRepository.save(reservation).getId();
}
الاستثناء المخصص عند امتلاء السعة
package com.amenity_reservation_system.exception;
public class CapacityFullException extends RuntimeException {
public CapacityFullException(String message) {
super(message);
}
}
نقاط تقنية مهمة لتحسين جودة المشروع
- استخدام Service Layer يحافظ على نظافة المشروع.
- الاعتماد على H2 مناسب جداً أثناء التطوير والتجربة.
- Thymeleaf خيار عملي لتطبيقات SSR في Spring Boot.
- Spring Security يوفر طبقة حماية قوية دون تعقيد مفرط في البداية.
- استعلامات JPA derived queries توفر سرعة في التطوير، لكن يجب الانتباه إلى طول الأسماء وقابلية الصيانة مستقبلاً.
رابط المستودع البرمجي
يمكنك الاطلاع على الشيفرة الكاملة للمشروع من خلال المستودع التالي:
https://github.com/yigiterinc/amenity-reservation-system.git
الخلاصة التقنية
إذا كنت تبحث عن طريقة عملية لبناء تطبيقات Java حديثة بسرعة، فإن Spring Boot يقدم توازناً ممتازاً بين السرعة، والمرونة، وقابلية التوسع. هذا المثال يوضح كيف يمكن دمج JPA وThymeleaf وSpring Security في مشروع واحد بجهد معقول جداً. تقنياً، أفضل ما في هذا النهج أنه يسمح لك بإطلاق نموذج أولي فعّال بسرعة، ثم تطويره لاحقاً إلى بنية أكثر احترافية دون الحاجة إلى إعادة البناء من الصفر.