Kotlin: شرح Spring Boot بكوتلن
آخر تحديث: 2026-08-26
Spring Boot + كوتلن هو الثنائي الذهبي للخدمات المصغرة الخلفية — يستخدم Charlie فئة data لهيئات الطلب/الاستجابة، ودوال وحدات التحكم suspend لواجهات برمجة التطبيقات غير الحظرية، والدوال الممتدة لجعل الكود أكثر اصطلاحية في كوتلن.
1. ما ستتعلمه
- تهيئة Spring Boot + كوتلن
- وحدات التحكم:
@RestController+ هيئات طلب/استجابة بفئة data - دعم الكوروتينات: دوال وحدات التحكم
suspend - JPA + كوتلن: إضافة مترجم
kotlin-jpa - تطبيق Charlie: خدمة مصغرة OrderProcessor
2. قصة مهندس معمارية حقيقي
(1) المشكلة: الكود المعياري في Java Spring Boot
وحدات تحكم Java Spring Boot الخاصة بـ Charlie كانت تحتاج 30+ سطرًا من POJO + getters/setters لكل طلب/استجابة. واجهات برمجة التطبيقات غير المتزامنة باستخدام تداخل CompletableFuture كانت صعبة الصيانة.
(2) حل كوتلن Spring Boot
KOTLIN
// Java: 30+ سطر لـ DTO طلب
public class CreateOrderRequest {
private String customerId;
private Double total;
// getter/setter × 2 = 8 أسطر
}
// كوتلن: سطر واحد لـ DTO طلب
data class CreateOrderRequest(val customerId: String, val total: Double)
// دالة تحكم suspend
@PostMapping
suspend fun createOrder(@RequestBody req: CreateOrderRequest): OrderResponse {
return orderService.createOrder(req) // غير حظرية!
}
فئة data + suspend تُقلل كود Spring Boot بنسبة 60%، وتُصبح واجهات برمجة التطبيقات غير المتزامنة سهلة القراءة مثل الكود المتزامن.
3. تهيئة Spring Boot + كوتلن
(1) build.gradle.kts
KOTLIN
plugins {
kotlin("jvm") version "1.9.22"
kotlin("plugin.spring") version "1.9.22" // دعم Spring
kotlin("plugin.jpa") version "1.9.22" // JPA بدون وسيطات
kotlin("plugin.serialization") version "1.9.22"
id("org.springframework.boot") version "3.2.1"
id("io.spring.dependency-management") version "1.1.4"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-webflux") // لدعم الكوروتينات
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
(2) نظرة عامة على إضافات كوتلن Spring
| الإضافة | الغرض |
|---|---|
kotlin-spring |
فتح تلقائي للفئات الموضحة بـ @Component، @Transactional، إلخ. |
kotlin-jpa |
توليد منشئات بدون وسيطات لفئات @Entity |
kotlin-serialization |
تفعيل تسلسل JSON لفئات data |
4. وحدات تحكم REST
(1) وحدة تحكم أساسية
KOTLIN
@RestController
@RequestMapping("/api/orders")
class OrderController(private val orderService: OrderService) {
@GetMapping
fun getAllOrders(): List<OrderResponse> = orderService.findAll()
@GetMapping("/{id}")
fun getOrder(@PathVariable id: String): OrderResponse =
orderService.findById(id) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@PostMapping
fun createOrder(@RequestBody request: CreateOrderRequest): OrderResponse =
orderService.create(request)
}
(2) هيئات طلب/استجابة بفئة data
KOTLIN
data class CreateOrderRequest(
val customerId: String,
val total: Double,
val items: List<OrderItemRequest> = emptyList()
)
data class OrderItemRequest(
val sku: String,
val quantity: Int,
val unitPrice: Double
)
data class OrderResponse(
val id: String,
val total: Double,
val status: String,
val customer: String
)
(3) Java DTO مقابل فئة data في كوتلن
| البُعد | Java DTO | فئة data في كوتلن |
|---|---|---|
| أسطر الكود | 30+ سطر | 3 أسطر |
| equals/hashCode | يدوي/Lombok | توليد تلقائي |
| الثبات | جهد يدوي | val افتراضي |
| القيم الافتراضية | تحميل زائد للدوال | القيم الافتراضية للمعلمات |
| تعيين JSON | توضيحات Jackson | jackson-module-kotlin تلقائي |
5. دعم الكوروتينات
(1) دوال تحكم Suspend
KOTLIN
// أضف تبعية webflux لدعم الكوروتينات
@RestController
@RequestMapping("/api/orders")
class OrderController(private val orderService: OrderService) {
// تحكم suspend: غير حظرية، تعمل على حلقة أحداث Netty
@GetMapping("/{id}")
suspend fun getOrder(@PathVariable id: String): OrderResponse {
return orderService.findByIdAsync(id)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@PostMapping
suspend fun createOrder(@RequestBody request: CreateOrderRequest): OrderResponse {
return orderService.createAsync(request)
}
// نقطة نهاية Flow: استجابة متدفقة
@GetMapping("/stream")
fun orderStream(): Flow<OrderResponse> = orderService.orderStream()
}
(2) سلسلة معالجة الطلبات في Spring Boot
flowchart TD
A[طلب HTTP] --> B[DispatcherServlet<br/>أو Netty]
B --> C[وحدة التحكم<br/>@RestController]
C --> D{suspend?}
D -->|نعم| E[سياق الكوروتين<br/>غير حظرية]
D -->|لا| F[خيط Servlet<br/>حظرية]
E --> G[طبقة الخدمة<br/>دوال suspend]
F --> G
G --> H[المستودع<br/>R2DBC / JPA]
H --> I[قاعدة البيانات]
(3) مقارنة الحظرية مقابل غير الحظرية
| البُعد | حظرية (MVC) | غير حظرية (WebFlux + كوروتينات) |
|---|---|---|
| نموذج الخيوط | خيط واحد لكل طلب | حلقة أحداث |
| حد التزامن | ~200 (تجمع خيوط) | ~100,000+ (كوروتينات) |
| أسلوب الكود | متزامن | suspend (يُقرأ كالمتزامن) |
| قاعدة البيانات | JPA (حظرية) | R2DBC (تفاعلية) |
| الإنتاجية | متوسطة | عالية |
6. JPA + كوتلن
(1) تعريف الكيان
KOTLIN
@Entity
@Table(name = "orders")
class OrderEntity(
@Id @GeneratedValue(strategy = GenerationType.UUID)
val id: String = "",
val total: Double = 0.0,
val status: String = "PENDING",
val customerId: String = "",
@CreationTimestamp
val createdAt: LocalDateTime = LocalDateTime.now()
)
// المستودع
interface OrderJpaRepository : JpaRepository<OrderEntity, String> {
fun findByCustomerId(customerId: String): List<OrderEntity>
fun countByStatus(status: String): Long
}
(2) إضافة no-arg
KOTLIN
// إضافة kotlin-jpa تولد تلقائيًا منشئًا بدون وسيطات لفئات @Entity
// بدونها، لا يمكن لـ JPA إنشاء فئات كوتلن (جميعها لديها معلمات منشئ)
7. مثال كامل: خدمة مصغرة OrderProcessor
KOTLIN
// ============================================
// OrderProcessor - خدمة مصغرة Spring Boot
// الميزة: REST API + suspend + فئة data
// ============================================
// --- النطاق ---
data class Order(val id: String, val total: Double, var status: String, val customerId: String)
// --- طلب/استجابة ---
data class CreateOrderRequest(val customerId: String, val total: Double)
data class OrderResponse(val id: String, val total: Double, val status: String, val customerId: String)
data class UpdateStatusRequest(val status: String)
// --- مستودع (محاكاة) ---
class OrderRepository {
private val storage = mutableMapOf<String, Order>()
fun save(order: Order): Order {
storage[order.id] = order
return order
}
fun findById(id: String): Order? = storage[id]
fun findAll(): List<Order> = storage.values.toList()
fun deleteById(id: String) { storage.remove(id) }
}
// --- خدمة ---
class OrderService(private val repo: OrderRepository) {
private var idCounter = 0L
fun create(request: CreateOrderRequest): Order {
val order = Order("ORD-${++idCounter}", request.total, "PENDING", request.customerId)
return repo.save(order)
}
fun findById(id: String): Order? = repo.findById(id)
fun findAll(): List<Order> = repo.findAll()
fun updateStatus(id: String, newStatus: String): Order {
val order = repo.findById(id) ?: throw NoSuchElementException("Order $id not found")
order.status = newStatus
return repo.save(order)
}
fun delete(id: String) = repo.deleteById(id)
}
// --- وحدة تحكم (محاكاة، تتطلب وقت تشغيل Spring Boot) ---
// @RestController
// @RequestMapping("/api/orders")
// class OrderController(private val orderService: OrderService) {
// @GetMapping fun getAll() = orderService.findAll().map { it.toResponse() }
// @GetMapping("/{id}") fun getOne(@PathVariable id: String) = orderService.findById(id)?.toResponse()
// @PostMapping fun create(@RequestBody req: CreateOrderRequest) = orderService.create(req).toResponse()
// }
// --- دالة ممتدة للتعيين ---
fun Order.toResponse() = OrderResponse(id, total, status, customerId)
// --- عرض توضيحي ---
fun main() {
val repo = OrderRepository()
val service = OrderService(repo)
println("=== عرض واجهة برمجة تطبيقات Spring Boot OrderProcessor ===\n")
// POST /api/orders
val order1 = service.create(CreateOrderRequest("CUST-001", 299.99))
println("POST /api/orders -> ${order1.toResponse()}")
val order2 = service.create(CreateOrderRequest("CUST-002", 15_000.00))
println("POST /api/orders -> ${order2.toResponse()}")
// GET /api/orders
println("\nGET /api/orders -> ${service.findAll().map { it.toResponse() }}")
// GET /api/orders/{id}
println("GET /api/orders/ORD-1 -> ${service.findById("ORD-1")?.toResponse()}")
// PATCH /api/orders/{id}/status
val updated = service.updateStatus("ORD-1", "CONFIRMED")
println("PATCH /api/orders/ORD-1/status -> ${updated.toResponse()}")
// ملخص
val revenue = service.findAll().filter { it.status != "CANCELLED" }.sumOf { it.total }
println("\nإجمالي الإيرادات: \$$revenue USD عبر ${service.findAll().size} طلبات")
}
الإخراج:
TEXT
📖 للعرض فقط
=== عرض واجهة برمجة تطبيقات Spring Boot OrderProcessor ===
POST /api/orders -> OrderResponse(id=ORD-1, total=299.99, status=PENDING, customerId=CUST-001)
POST /api/orders -> OrderResponse(id=ORD-2, total=15000.0, status=PENDING, customerId=CUST-002)
GET /api/orders -> [OrderResponse(id=ORD-1, total=299.99, status=PENDING, customerId=CUST-001), OrderResponse(id=ORD-2, total=15000.0, status=PENDING, customerId=CUST-002)]
GET /api/orders/ORD-1 -> OrderResponse(id=ORD-1, total=299.99, status=PENDING, customerId=CUST-001)
PATCH /api/orders/ORD-1/status -> OrderResponse(id=ORD-1, total=299.99, status=CONFIRMED, customerId=CUST-001)
إجمالي الإيرادات: $15299.99 USD عبر 2 طلبات
8. أمثلة عملية سريعة
▶ مثال: Spring Boot Application أساسي
KOTLIN
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class OrderProcessorApplication
fun main(args: Array<String>) {
runApplication<OrderProcessorApplication>(*args)
}
KOTLIN
// Controller بسيط
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/v1")
class HelloController {
@GetMapping("/hello")
fun hello(): Map<String, String> = mapOf("message" to "Hello, Kotlin!")
@GetMapping("/echo/{name}")
fun echo(@PathVariable name: String): Map<String, String> = mapOf("echo" to name)
}
**الإخراج:
TEXT
📖 للعرض فقط
GET /api/v1/hello → {"message":"Hello, Kotlin!"}
GET /api/v1/echo/world → {"echo":"world"}
▶ مثال: REST API كامل لـ Order
KOTLIN
import org.springframework.http.*
import org.springframework.web.bind.annotation.*
data class OrderRequest(
val customerId: String,
val total: Double,
val currency: String = "USD"
)
data class OrderResponse(
val id: Long,
val customerId: String,
val total: Double,
val currency: String,
val status: String
)
@RestController
@RequestMapping("/api/orders")
class OrderController(private val orderService: OrderService) {
@GetMapping
fun listAll(): List<OrderResponse> = orderService.findAll()
@GetMapping("/{id}")
fun findOne(@PathVariable id: Long): ResponseEntity<OrderResponse> {
val order = orderService.findById(id)
return if (order != null) {
ResponseEntity.ok(order)
} else {
ResponseEntity.notFound().build()
}
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun create(@RequestBody request: OrderRequest): OrderResponse =
orderService.create(request)
@PutMapping("/{id}")
fun update(@PathVariable id: Long, @RequestBody request: OrderRequest): ResponseEntity<OrderResponse> {
val updated = orderService.update(id, request)
return ResponseEntity.ok(updated)
}
@DeleteMapping("/{id}")
fun delete(@PathVariable id: Long): ResponseEntity<Void> {
orderService.delete(id)
return ResponseEntity.noContent().build()
}
}
**الإخراج:
TEXT
📖 للعرض فقط
GET /api/orders → [{id:1,...}, {id:2,...}]
POST /api/orders → 201 Created
▶ مثال: Service Layer مع Transaction
KOTLIN
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.data.repository.*
interface OrderJpaRepository : JpaRepository<OrderEntity, Long>
@Service
class OrderService(private val repo: OrderJpaRepository) {
@Transactional(readOnly = true)
fun findAll(): List<OrderResponse> =
repo.findAll().map { it.toResponse() }
@Transactional(readOnly = true)
fun findById(id: Long): OrderResponse? =
repo.findById(id).orElse(null)?.toResponse()
@Transactional
fun create(request: OrderRequest): OrderResponse {
val entity = OrderEntity(
customerId = request.customerId,
total = request.total,
currency = request.currency,
status = "PENDING"
)
return repo.save(entity).toResponse()
}
@Transactional
fun update(id: Long, request: OrderRequest): OrderResponse {
val entity = repo.findById(id).orElseThrow()
entity.customerId = request.customerId
entity.total = request.total
entity.currency = request.currency
return repo.save(entity).toResponse()
}
@Transactional
fun delete(id: Long) {
repo.deleteById(id)
}
}
@Entity
data class OrderEntity(
@Id @GeneratedValue
var id: Long = 0,
var customerId: String = "",
var total: Double = 0.0,
var currency: String = "USD",
var status: String = "PENDING"
) {
fun toResponse() = OrderResponse(id, customerId, total, currency, status)
}
**الإخراج:
TEXT
📖 للعرض فقط
▶ مثال: Validation مع Bean Validation
KOTLIN
import jakarta.validation.constraints.*
import org.springframework.validation.annotation.Validated
data class CreateOrderDto(
@field:NotBlank
@field:Size(min = 3, max = 50)
val customerId: String,
@field:Positive
@field:Max(1_000_000)
val total: Double,
@field:Pattern(regexp = "USD|EUR|GBP")
val currency: String = "USD"
)
@RestController
@RequestMapping("/api/orders")
class OrderController {
@PostMapping
fun create(@Valid @RequestBody dto: CreateOrderDto): ResponseEntity<Map<String, Any>> {
// dto تم التحقق منه تلقائيًا
return ResponseEntity.status(201).body(mapOf(
"id" to 1L,
"status" to "created",
"customerId" to dto.customerId
))
}
}
BASH
# طلب صحيح
curl -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{"customerId":"C001","total":1500,"currency":"USD"}'
# → 201 Created
# طلب غير صحيح (total سالب)
curl -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{"customerId":"C001","total":-100,"currency":"USD"}'
# → 400 Bad Request
**الإخراج:
TEXT
📖 للعرض فقط
▶ مثال: Coroutines مع WebFlux
KOTLIN
import kotlinx.coroutines.*
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.*
import org.springframework.web.reactive.function.server.*
@RestController
class ReactiveOrderController(private val service: OrderService) {
@GetMapping("/api/reactive/orders", produces = [MediaType.APPLICATION_JSON_VALUE])
suspend fun listAll(): List<OrderResponse> = service.findAll()
@GetMapping("/api/reactive/orders/{id}")
suspend fun findOne(@PathVariable id: Long): ResponseEntity<OrderResponse> {
val order = service.findById(id)
return order?.let { ResponseEntity.ok(it) }
?: ResponseEntity.notFound().build()
}
@PostMapping("/api/reactive/orders")
suspend fun create(@RequestBody request: OrderRequest): ResponseEntity<OrderResponse> {
val created = service.create(request)
return ResponseEntity.status(201).body(created)
}
}
// مع HandlerFunction DSL
fun orderRoutes(service: OrderService) = coRouter {
"/api/co/orders".nest {
GET("") {
val orders = service.findAll()
ok().bodyValueAndAwait(orders)
}
GET("/{id}") {
val id = it.pathVariable("id").toLong()
val order = service.findById(id)
if (order != null) ok().bodyValueAndAwait(order)
else notFound().buildAndAwait()
}
}
}
**الإخراج:
TEXT
📖 للعرض فقط
▶ مثال: Configuration Properties
KOTLIN
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Configuration
@ConfigurationProperties(prefix = "app")
data class AppProperties(
val name: String = "OrderProcessor",
val maxConnections: Int = 100,
val timeout: Long = 30000,
val database: Database = Database(),
val features: Features = Features()
) {
data class Database(
val host: String = "localhost",
val port: Int = 5432,
val name: String = "orders"
)
data class Features(
val enableCache: Boolean = true,
val enableMetrics: Boolean = true
)
}
@Configuration
@EnableConfigurationProperties(AppProperties::class)
class AppConfig(private val props: AppProperties) {
fun printConfig() {
println("App: ${props.name}")
println("Database: ${props.database.host}:${props.database.port}/${props.database.name}")
println("Features: cache=${props.features.enableCache}, metrics=${props.features.enableMetrics}")
}
}
// application.yml
// app:
// name: "OrderProcessor Pro"
// max-connections: 200
// database:
// host: "prod-db.example.com"
// port: 5432
**الإخراج:
TEXT
📖 للعرض فقط
App: OrderProcessor Pro
Database: prod-db.example.com:5432/orders
Features: cache=true, metrics=true
▶ مثال: Exception Handling
KOTLIN
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.*
@ControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException::class)
@ResponseStatus(HttpStatus.NOT_FOUND)
fun handleNotFound(ex: ResourceNotFoundException): Map<String, Any> = mapOf(
"error" to "ResourceNotFound",
"message" to (ex.message ?: "Resource not found"),
"status" to 404
)
@ExceptionHandler(ValidationException::class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
fun handleValidation(ex: ValidationException): Map<String, Any> = mapOf(
"error" to "ValidationError",
"message" to (ex.message ?: "Validation failed"),
"status" to 400,
"violations" to (ex.violations ?: emptyList<String>())
)
@ExceptionHandler(Exception::class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
fun handleGeneric(ex: Exception): Map<String, Any> = mapOf(
"error" to "InternalError",
"message" to (ex.message ?: "An error occurred"),
"status" to 500
)
}
class ResourceNotFoundException(id: Any) : RuntimeException("Resource $id not found")
class ValidationException(message: String, val violations: List<String> = emptyList()) : RuntimeException(message)
**الإخراج:
TEXT
📖 للعرض فقط
404 Not Found: {"error":"ResourceNotFound","message":"Order 999 not found","status":404}
500 Internal: {"error":"InternalError","message":"Database timeout","status":500}
❓ أسئلة شائعة
س هل يجب استخدام MVC أو WebFlux لـ Spring Boot؟
ج يُوصى بـ WebFlux + كوروتينات للمشاريع الجديدة —
suspend يُقرأ ككود متزامن لكنه يؤدي عمليات غير حظرية. إذا كان فريقك أكثر دراية بـ JPA ولا يحتاج تزامنًا عاليًا، MVC + كوروتينات يعمل أيضًا.س هل يمكن استخدام فئة data في كوتلن ككيان JPA؟
ج ممكن لكن مع قيود — فئات data غير قابلة للتغيير بطبيعتها، بينما JPA يحتاج كيانات قابلة للتغيير للتحقق من التعديلات. يُوصى: استخدم
class عادي + خصائص var للكيانات، وفئة data لـ DTOs.س ماذا تفعل إضافة
kotlin-spring؟ج تفتح تلقائيًا الفئات الموضحة بـ
@Component، @Transactional، إلخ، حيث يحتاج Spring AOP لإنشاء وكلاء (مما يتطلب فئات قابلة للوراثة).س هل تتطلب دوال تحكمsuspend تبعية WebFlux؟
ج نعم. Spring MVC لا يدعم
suspend أصلاً — تبعيات WebFlux مطلوبة. Spring 6.1+ حسّن الدعم، لكن WebFlux لا يزال مُوصى به.س كيف أستخدم كوروتينات كوتلن في Spring Boot؟
ج أضف تبعية
spring-boot-starter-webflux، علّم دوال التحكم بـ suspend، استخدم دوال suspend في طبقة الخدمة، واستخدم R2DBC أو امتدادات الكوروتينات للمستودع.س هل
jackson-module-kotlin إلزامي؟ج يُوصى به بشدة. بدونه، لا يستطيع Jackson إلغاء تسلسل فئات data في كوتلن بشكل صحيح (معلمات المنشئ، القابلية لـ null، ومعالجة القيم الافتراضية تفشل جميعًا).
📖 ملخص
- تهيئة Spring Boot + كوتلن تتطلب إضافات
kotlin-springوkotlin-jpa - فئة data لهيئات طلب/استجابة DTOs: سطر واحد يستبدل 30+ سطر من Java
- دوال تحكم
suspend+ WebFlux تُفعل واجهات برمجة تطبيقات غير حظرية - إضافة
kotlin-jpaتولد منشئات بدون وسيطات للكيانات - الدوال الممتدة مثل
toResponse()تفصل نماذج النطاق عن نماذج API - إنتاجية WebFlux + كوروتينات تتفوق بكثير على MVC + تجمعات الخيوط
📝 تمارين
- مبتدئ (⭐): عرّف
CreateUserRequestوUserResponseباستخدام فئة data، واكتب وحدة تحكم Spring Boot. تلميح:@RestController+@PostMapping - متوسط (⭐⭐): نفّذ دالة تحكم
suspendتستدعي دالة خدمةsuspendلجلب طلب. تلميح:suspend fun getOrder(@PathVariable id: String) - متقدم (⭐⭐⭐): نفّذ واجهة برمجة تطبيقات REST CRUD كاملة + كوروتينات + R2DBC (أو مستودع محاكى)، بما في ذلك معالجة الاستثناءات والتحقق. تلميح:
@RestControllerAdvice+@Valid