Kotlin: شرح Spring Boot بكوتلن

آخر تحديث: 2026-08-26

Spring Boot + كوتلن هو الثنائي الذهبي للخدمات المصغرة الخلفية — يستخدم Charlie فئة data لهيئات الطلب/الاستجابة، ودوال وحدات التحكم suspend لواجهات برمجة التطبيقات غير الحظرية، والدوال الممتدة لجعل الكود أكثر اصطلاحية في كوتلن.

1. ما ستتعلمه


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

100%
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، ومعالجة القيم الافتراضية تفشل جميعًا).

📖 ملخص


📝 تمارين

  1. مبتدئ (⭐): عرّف CreateUserRequest و UserResponse باستخدام فئة data، واكتب وحدة تحكم Spring Boot. تلميح: @RestController + @PostMapping
  2. متوسط (⭐⭐): نفّذ دالة تحكم suspend تستدعي دالة خدمة suspend لجلب طلب. تلميح: suspend fun getOrder(@PathVariable id: String)
  3. متقدم (⭐⭐⭐): نفّذ واجهة برمجة تطبيقات REST CRUD كاملة + كوروتينات + R2DBC (أو مستودع محاكى)، بما في ذلك معالجة الاستثناءات والتحقق. تلميح: @RestControllerAdvice + @Valid

← السابق | التالي →

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%