مشروع: أول تطبيق Spring Boot لك
كل ما تعلمته عبر هذا البرنامج التعليمي — المشغّلات (starters)، والإعداد التلقائي، والخادم المدمج، وخصائص التطبيق، وخطّافات دورة الحياة، والتسجيل، وأدوات المطوّر — يتجمّع كلّه في تطبيق واحد كامل وقابل للتشغيل. يرشدك هذا الدرس خلال بناء واجهة برمجية REST لإدارة المهام صغيرة لكنها واقعية من الصفر: إنشاء المشروع، وربط الطبقات، وفهم كل قرار، وتشغيله من البداية إلى النهاية. في نهاية الدرس سيكون لديك شيء قابل للنشر، لا مجرّد تطبيق "مرحبًا بالعالم".
ما الذي نبنيه
واجهة برمجية JSON للتعامل مع قائمة مهام. تدعم أربع عمليات:
- GET /api/tasks — سرد جميع المهام
- GET /api/tasks/{id} — جلب مهمة واحدة
- POST /api/tasks — إنشاء مهمة
- DELETE /api/tasks/{id} — حذف مهمة
تعتمد الحفظ على قاعدة بيانات H2 في الذاكرة (لا إعداد مطلوب) مع Spring Data JPA الذي يتولّى كل SQL. يعرض التطبيق نقطة نهاية فحص الصحة عبر Actuator، ويستخدم CommandLineRunner مخصّصًا لزرع بيانات تجريبية عند الإطلاق، ويقرأ منفذ الخادم من application.properties.
لماذا هذا المكدّس؟ Spring Web + Spring Data JPA + H2 + Actuator يغطّي أكثر التركيبات شيوعًا في المشاريع الفعلية. استبدال H2 بـ PostgreSQL لاحقًا يستلزم تغيير تبعية واحدة فقط وخاصيّتين — بقية الكود متطابق.
الخطوة الأولى — بوتسترابنة المشروع
أنشئ المشروع على start.spring.io (أو استخدم Spring Initializr داخل بيئة التطوير) بهذه الإعدادات:
- المشروع: Maven | اللغة: Java | Spring Boot: 3.3.x
- المجموعة:
com.example | المُعرَّف: taskmanager
- التبعيات: Spring Web، Spring Data JPA، H2 Database، Spring Boot Actuator، Spring Boot DevTools
تبدو مشغّلات pom.xml المُولَّدة هكذا (الإصدارات تديرها BOM الخاصة بـ Boot):
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
الخطوة الثانية — خصائص التطبيق
افتح src/main/resources/application.properties وأضف:
# الخادم
server.port=8080
spring.application.name=task-manager
# قاعدة بيانات H2 في الذاكرة
spring.datasource.url=jdbc:h2:mem:taskdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# JPA / Hibernate
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# وحدة تحكم H2 (مفيدة أثناء التطوير)
spring.h2.console.enabled=true
# Actuator — كشف health و info عبر HTTP
management.endpoints.web.exposure.include=health,info
ddl-auto=create-drop يُعيد إنشاء المخطّط في كل بدء تشغيل ويحذفه عند الإغلاق — مثالي لـ H2 في بيئة التطوير. لقاعدة بيانات حقيقية استخدم validate (أو none إذا كنت تُدير التهجيرات بـ Flyway أو Liquibase).
الخطوة الثالثة — نموذج النطاق
الكيان (entity) في JPA هو فئة Java عادية مُزيَّنة بـ @Entity. سيُولِّد Spring Data JPA الجدول المقابل تلقائيًا:
package com.example.taskmanager.model;
import jakarta.persistence.*;
@Entity
@Table(name = "tasks")
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
private boolean completed = false;
// مطلوب بواسطة JPA
protected Task() {}
public Task(String title) {
this.title = title;
}
// Getters و Setters
public Long getId() { return id; }
public String getTitle() { return title; }
public void setTitle(String t) { this.title = t; }
public boolean isCompleted() { return completed; }
public void setCompleted(boolean c) { this.completed = c; }
}
أمران جديران بالملاحظة: المُنشئ بدون وسيطات protected (تتطلبه JPA لكن لا ينبغي استدعاؤه مباشرةً)، وGenerationType.IDENTITY يُفوّض الزيادة التلقائية للقاعدة، وهو الخيار الصحيح لـ H2 وأغلب قواعد البيانات العلائقية.
الخطوة الرابعة — المستودع
يُولِّد Spring Data JPA تنفيذًا كاملًا لـ CRUD في وقت التشغيل من تصريح واجهة واحدة فقط:
package com.example.taskmanager.repository;
import com.example.taskmanager.model.Task;
import org.springframework.data.jpa.repository.JpaRepository;
public interface TaskRepository extends JpaRepository<Task, Long> {
// JpaRepository توفّر بالفعل: findAll, findById, save, deleteById, count, ...
// أضف استعلامات مخصّصة هنا عند الحاجة، مثل:
// List<Task> findByCompletedFalse();
}
لا توجد فئة تنفيذ لكتابتها. يكتشف الإعداد التلقائي لـ Spring Boot وجود JpaRepository في مسار الفئات ويسجّل بروكسي Bean تلقائيًا.
الخطوة الخامسة — طبقة الخدمة
تحتوي الخدمة على منطق الأعمال وتُبقي المتحكّم نظيفًا. وهي المكان المناسب للمعاملات وأي قواعد تمتد عبر استدعاءات مستودع متعددة:
package com.example.taskmanager.service;
import com.example.taskmanager.model.Task;
import com.example.taskmanager.repository.TaskRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(readOnly = true)
public class TaskService {
private final TaskRepository repo;
public TaskService(TaskRepository repo) { // حقن المُنشئ — لا حاجة لـ @Autowired
this.repo = repo;
}
public List<Task> findAll() {
return repo.findAll();
}
public Task findById(Long id) {
return repo.findById(id)
.orElseThrow(() -> new RuntimeException("Task not found: " + id));
}
@Transactional // يتجاوز readOnly لعمليات الكتابة
public Task create(String title) {
return repo.save(new Task(title));
}
@Transactional
public void delete(Long id) {
repo.deleteById(id);
}
}
لماذا readOnly = true على مستوى الفئة؟ يُخبر Hibernate بتخطّي فحص التغييرات (dirty-checking) في عمليات القراءة، مما يقلّل التكلفة. تجاوزه بـ @Transactional (بدون علامة) فقط حيث تكتب البيانات.
الخطوة السادسة — متحكّم REST
يُترجم المتحكّم طلبات HTTP إلى استدعاءات للخدمة ويحوّل قيم الإرجاع إلى استجابات JSON:
package com.example.taskmanager.controller;
import com.example.taskmanager.model.Task;
import com.example.taskmanager.service.TaskService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final TaskService service;
public TaskController(TaskService service) {
this.service = service;
}
@GetMapping
public List<Task> list() {
return service.findAll();
}
@GetMapping("/{id}")
public Task get(@PathVariable Long id) {
return service.findById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED) // يُرجع HTTP 201 بدلًا من 200
public Task create(@RequestParam String title) {
return service.create(title);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) // 204 — نجاح بدون جسم استجابة
public void delete(@PathVariable Long id) {
service.delete(id);
}
}
@RestController اختصار لـ @Controller + @ResponseBody. كل قيمة إرجاع لدالة تُسلسَل إلى JSON بواسطة Jackson (المُضمَّن ضمنيًا في spring-boot-starter-web) دون أي إعداد إضافي.
الخطوة السابعة — زرع البيانات بـ CommandLineRunner
يعمل Bean من نوع CommandLineRunner بعد بدء سياق التطبيق بالكامل، مما يجعله مثاليًا لملء قاعدة البيانات لأغراض التطوير أو العرض التوضيحي:
package com.example.taskmanager;
import com.example.taskmanager.service.TaskService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DataSeeder {
@Bean
CommandLineRunner seed(TaskService service) {
return args -> {
service.create("Set up Spring Boot project");
service.create("Write the Task entity");
service.create("Build the REST API");
System.out.println("Sample tasks loaded.");
};
}
}
الخطوة الثامنة — الفئة الرئيسية
نقطة الدخول مُولَّدة لك بالفعل. لاحظ أنها تمامًا كما نوقشت في الدرس الأول — لا شيء يستلزم التعديل:
package com.example.taskmanager;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TaskManagerApplication {
public static void main(String[] args) {
SpringApplication.run(TaskManagerApplication.class, args);
}
}
الخطوة التاسعة — التشغيل والتحقق
ابدأ التطبيق بـ mvn spring-boot:run (أو شغّل الفئة الرئيسية من بيئة التطوير). ستشاهد Hibernate يطبع DDL لـ CREATE TABLE tasks، ثم سطر السجلّ الخاص بـ DataSeeder، وأخيرًا منفذ Tomcat. ثم اختبر بـ curl:
# إنشاء مهمة
curl -X POST "http://localhost:8080/api/tasks?title=Learn+Spring+Boot"
# سرد جميع المهام
curl http://localhost:8080/api/tasks
# جلب المهمة ذات id يساوي 1
curl http://localhost:8080/api/tasks/1
# حذف المهمة ذات id يساوي 2
curl -X DELETE http://localhost:8080/api/tasks/2
# فحص الصحة عبر Actuator
curl http://localhost:8080/actuator/health
بيانات H2 مؤقتة. نظرًا لاستخدامنا ddl-auto=create-drop مع عنوان URL في الذاكرة، تختفي جميع البيانات عند انتهاء JVM. للحفظ عبر عمليات إعادة التشغيل أثناء التطوير، انتقل إلى jdbc:h2:file:./data/taskdb واضبط ddl-auto=update.
مراجعة هيكل المشروع
يتبع التخطيط النهائي للحزم معمارية الطبقات المعيارية:
src/main/java/com/example/taskmanager/
TaskManagerApplication.java <-- نقطة دخول @SpringBootApplication
DataSeeder.java <-- Bean من نوع CommandLineRunner لزرع البيانات
model/
Task.java <-- كائن النطاق @Entity
repository/
TaskRepository.java <-- واجهة JpaRepository
service/
TaskService.java <-- منطق الأعمال @Service
controller/
TaskController.java <-- طبقة HTTP @RestController
src/main/resources/
application.properties <-- كل الإعدادات في مكان واحد
لكل طبقة مسؤولية واحدة وتعتمد فقط على الطبقة التي تحتها. المتحكّم لا يعرف شيئًا عن JPA؛ والخدمة لا تعرف شيئًا عن HTTP. هذا الفصل يجعل كل فئة سهلة الاختبار بمعزل عن الأخريات.
ما يمكنك تجربته بعد ذلك
- أضف نقطة نهاية
PUT /api/tasks/{id} لتعليم مهمة كمكتملة.
- استبدل
@RequestParam بجسم طلب JSON باستخدام @RequestBody وسجلّ DTO.
- أضف
@ExceptionHandler مخصّصًا (أو @ControllerAdvice) لإرجاع خطأ JSON منظَّم عند عدم العثور على المهمة بدلًا من استجابة 500.
- استبدل H2 بـ PostgreSQL: أضف تبعية مشغّل PostgreSQL، وغيّر عنوان URL للمصدر وبيانات الاعتماد في
application.properties، واضبط ddl-auto=validate، وشغّل تهجير Flyway — كود Java لا يُمسّ.
هذه هي القوة الحقيقية لـ Spring Boot. مخاوف البنية التحتية — تجميع الاتصالات، وتسلسل JSON، وإدارة المعاملات، وإنشاء المخطّط — يتولّاها الإعداد التلقائي. أنت تكتب منطق الأعمال؛ والـ Boot يُركّب السباكة. كل درس في هذا البرنامج التعليمي شرح جزءًا من تلك السباكة حتى تتمكّن من فهمها وتخصيصها واستكشاف أخطائها عند الحاجة.