ข้าม​ไป​ยัง​เนื้อหา

annotation ครบ​ทุก​ตัว แต่​แอป​ไม่​ส​ตาร์ต

บท​ที่ 1 ติด @Cacheable ลงบน method เดิม​แล้ว​วัดผล บท​นี้​ถอย​กลับ​ไป​หนึ่ง​ก้าว ไป​ที่​คำถาม​ที่มา​ก่อน​การ​วัด

ติด annotation ครบ​ทุก​ตัว​แล้ว แอป​ส​ตาร์ต​หรือ​ยัง

ชั้น​ที่​จำ​คำ​ตอบ​ต้อง​มี​สวิตช์​เปิด​หนึ่ง​ตัว และ​บท​นี้​ใส่​สวิตช์​นั้น​ครบ​ก่อน​จะ​แตะ​อย่าง​อื่น

ทรง​ที่ probe วัด · สวิตช์​ของ​ชั้น cache ทั้ง​ชั้น
package clinic.cache
import org.springframework.cache.annotation.EnableCaching
import org.springframework.context.annotation.Configuration
/** หนึ่งบรรทัดที่เปิดชั้น cache ทั้งชั้น */
@Configuration
@EnableCaching
class CacheConfig

@EnableCaching@EnableCachingannotation ที่​เปิด​กลไก cache ให้​ทั้ง​แอป · ติด​ครบ​แล้ว​แต่​ไม่มี CacheManager สัก​ตัว แอป​จะ​ไม่​ส​ตาร์ต ไม่ใช่​ทำงาน​แบบ​ไม่​เก็บ​อะไร คือ​บรรทัด​ที่​บอก Spring ว่า​ให้​อ่าน annotation ของ cache ที่​ติด​อยู่​บน method ทั้ง​แอป ถ้า​ไม่มี​บรรทัด​นี้ @Cacheable ที่​ติด​ไว้​ก็​เป็น​แค่​ตัว​อักษร

ฝั่ง service · เหมือน​บท​ที่ 1 ทุก​ตัว​อักษร
@Service
class VisitService(private val repo: VisitRepo, private val tenant: CurrentTenant) {
@Cacheable("visits")
fun visits(): List<Visit> = repo.findByTenantId(tenant.id)
}

ครบ​สอง​ที่​แล้ว และ build.gradle.kts ยัง​เป็น​ตัว​เดิม​ของ​คอร์ส​ก่อน​หน้า​ทุก​บรรทัด ทรง​นี้ ไม่​ส​ตาร์ต

ข้อความ​ไม่​ได้​บอก​แค่​ว่า​อะไร​พัง มัน​บอก​ทางออก​ไว้​สอง​ทางใน​ประโยค​เดียวกัน

สิ่ง​ที่ probe อ่าน​ได้​จาก​ทรง​ที่​ไม่มี starter
NoSuchBeanDefinitionException: No qualifying bean of type
'org.springframework.cache.CacheManager' available: no CacheResolver specified -
register a CacheManager bean or remove the @EnableCaching annotation from your configuration.

CacheManagerCacheManagerbean ที่ Spring ตาม​หา​เมื่อ​กลไก cache ถูก​เปิด · ข้อความ​ตอน​ไม่มี​มัน​คือ NoSuchBeanDefinitionException ซึ่ง​เอ่ย​ทาง​แก้​ไว้​ใน​ตัว​เอง คือ​ลง​ทะเบียน bean หรือ​ถอด annotation ออก คือ bean ที่​ชั้น​นี้​ต้อง​มี​เพื่อ​ไป​หยิบ​ช่อง​เก็บ​ของ​ออก​มา​ตาม​ชื่อ cache เมื่อ​ไม่มี​ใคร​สร้าง​มัน​ขึ้น​มา @EnableCaching ก็​ไม่มี​อะไร​ให้​ทำงาน​ด้วย

ทางออก​ที่​ข้อความ​เสนอ​ไว้​มี​สอง​ทาง

  • register a CacheManager bean เข้าไป​เอง
  • ถอด @EnableCaching ออก​จาก configuration

ทาง​ที่​สาม​ซึ่ง​ข้อความ​ไม่​ได้​เอ่ย คือ​ทาง​ที่​บท​นี้​เดิน ให้ Boot สร้าง bean ตัว​นั้น​ให้​เอง

เอกสาร​พูด​ถึง​ความ​สะดวก และ​ประโยค​ที่​มัน​เขียน​ไว้​ก็​เป็น​ความ​จริง​ตาม​ที่​มัน​เขียน

เอกสาร​เรื่อง caching ของ Spring Boot เขียน​สอง​ประโยค​ที่​เกี่ยว​กับ​ทรง​นี้​ตรงๆ

  • “Spring Boot auto-configures the cache infrastructure as long as caching support is enabled by using the @EnableCaching annotation.”
  • “Use the spring-boot-starter-cache starter to quickly add basic caching dependencies.”

ประโยค​แรก​วาง​เงื่อนไข​ไว้​ที่ annotation ส่วน​ประโยค​ที่​สอง​พูด​ถึง starter ใน​ฐานะ​ความ​สะดวก คำ​ว่า quickly ชวน​ให้​อ่าน​ว่า starter เป็น​ตัว​ช่วย ไม่ใช่​เงื่อนไข

probe เขียน code Kotlin เท่า​กัน​ทุก byte ทั้ง​สอง​ทรง แล้ว​เทียบ runtimeClasspath ที่​ได้

build.gradle.kts · บรรทัด​เดียว​ที่​ต่าง​กัน​ระหว่าง​สอง​ทรง
implementation("org.springframework.boot:spring-boot-starter-cache")
ช่อง​ที่​วัดมี starterไม่มี starter
module ของ Boot ที่​ชื่อ​มี​คำ​ว่า cache บน runtimeClasspathspring-boot-cache 4.1.1 อยู่​ใน​รายการไม่มี​สัก​ตัว
แอป​ส​ตาร์ตส​ตาร์ตไม่​ส​ตาร์ต
bean ของ CacheManagerมีไม่มี

auto-configuration ของ cache อยู่​ใน module ที่​หาย​ไป​ตัว​นั้น ไม่มี module ก็​ไม่มี​ของ​ที่​จะ สร้าง bean ให้ และ​ไม่มี bean แอป​ก็​ไม่​ผ่าน​ขั้น​ตอน​ส​ตาร์ต

สอง​ข้อ​นี้​จริง​พร้อม​กัน เอกสาร​พูด​ถึง​ความ​สะดวก และ​สิ่ง​ที่​วัด​ได้​คือ​ใน Boot 4 ความ​สะดวก​นั้น​เป็น​เงื่อนไข​จำเป็น ไม่ใช่​ทาง​ลัด​ที่​ข้าม​ได้

ตั้ง​ต้น​ที่ annotation ชุด​เดียวกัน แล้ว​แยก​ทาง​ที่​คำถาม​ว่า build file มี starter หรือ​ไม่

flowchart TB
  S["@EnableCaching และ @Cacheable ครบทุกตัว"] --> Q{"build file มี starter ของ cache ไหม"}
  Q -->|"ไม่มี"| N1["ไม่มี module ของ Boot ที่ชื่อมีคำว่า cache บน runtimeClasspath"]
  N1 --> N2["ไม่มี auto-configuration ของ cache"]
  N2 --> N3["ไม่มี bean ของ CacheManager"]
  N3 --> N4["แอปไม่สตาร์ต · NoSuchBeanDefinitionException"]
  Q -->|"มี"| Y1["spring-boot-cache 4.1.1 อยู่บน runtimeClasspath"]
  Y1 --> Y2["auto-configuration สร้าง ConcurrentMapCacheManager ให้"]
  Y2 --> Y3["แอปสตาร์ต · cache ชื่อ visits พร้อมใช้"]

คำ​บรรยาย​ภาพ: annotation ชุด​เดียวกัน​พา​ไป​ได้​สอง​ปลาย​ทาง สิ่ง​ที่​ตัดสิน​คือ​บรรทัด​ใน build file ไม่ใช่​บรรทัด​ใน code

ใส่ starter แล้ว​สิ่ง​ที่​ฉีด​เข้า​มา​ได้​คือ​ตัว​ที่​เก็บ​ของ​ไว้​ใน map ตัว​หนึ่ง

ใน​ทรง​ที่​มี starter bean ที่​ฉีด​ได้​จริง​คือ ConcurrentMapCacheManagerConcurrentMapCacheManagerCacheManager ที่ starter เลือก​ให้​เมื่อ​ไม่มี​ใคร​กำหนด​เอง · เก็บ​ค่า​ไว้​ใน​หน่วย​ความ​จำ​ของ process เดียว ไม่มี​ที่​เก็บ​ภายนอก​และ​ไม่มี​ใคร​แชร์​ด้วย และ​ชื่อ cache ที่​มัน​ถือ​อยู่​คือ [visits] ตรง​กับ​ชื่อ​ที่​เขียน​ไว้​ใน @Cacheable("visits")

เอกสาร​บอก​ที่มา​ของ​ตัว​นี้​ไว้​ตรงๆ — “If none of the other providers can be found, a simple implementation using a ConcurrentHashMap as the cache store is configured.”

ไม่​ได้​ประกาศ provider ไหน​ไว้​เลย จึง​ได้​ตัว​ที่​ใช้ ConcurrentHashMap เป็น​ที่​เก็บ นี่​คือ​ค่า​เริ่ม​ต้น ไม่ใช่​ตัว​เลือก​ที่​ใคร​ใน​ทีม​เลือก

ราคา​ของ​ค่า​เริ่ม​ต้น​นี้​จ่าย​ทีหลัง ของ​ที่​เก็บ​อยู่​คือ​อ็อบเจ็กต์​ใน process เดียว​กับ​แอป และบท​ที่ 5 วัด​ว่า​มัน​ยื่น อ็อบเจ็กต์​ตัว​เดิม​ออก​ไป​ให้​ผู้​เรียก ไม่ใช่​สำเนา

รู้​แล้ว​ว่า​อะไร​ทำ​ให้​ส​ตาร์ต ยัง​ไม่รู้​ว่า​ใคร​เป็น​คน​ตัดสิน​ว่า​คำขอ​สอง​ใบ​ถาม​เรื่อง​เดียวกัน

สิ่ง​ที่​บท​นี้​ปิด​คือ​คำถาม​เชิงกล​ไก​ของ​การ​ติด​ตั้ง ทรง cached ของ​บท​ที่ 1 ต้อง​มี​ของ​ครบ​สี่​อย่าง คือ @EnableCaching บน CacheConfig · @Cacheable บน visits() · module ของ cache หนึ่ง​ตัว และ bean ของ CacheManager หนึ่ง​ตัว ขาด​อย่าง​ใด​อย่าง​หนึ่ง​ก็​ไม่​ได้​ทรง​นั้น

สิ่ง​ที่​ยัง​เปิด​อยู่​คือ​รู​ที่​บท​ที่ 1 วัด​ไว้ ทรง​ที่​ส​ตาร์ต​ได้​ใน​บท​นี้ คือ​ทรง​เดียว​กับ​ที่ clinic-b ได้​แถว​ของ clinic-a กลับ​ไป การ​ใส่ starter ไม่​ได้​แก้​ข้อ​นั้น​สัก​ช่อง

ConcurrentMapCacheManager ที่​เพิ่ง​ได้​มา ตัดสิน​ใจ​แทน​เรา​ไป​แล้ว​หนึ่ง​เรื่อง คือ​กุญแจ ของ​แต่ละ​คำขอ​หน้าตา​เป็น​อย่างไร บท​ที่ 3 เปิด​กล่อง​นั้น​ดู


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • Spring Boot — Caching (เข้า​ถึง 2026-09-04) — ที่มา​ของ​ทั้ง​สาม​ประโยค​ที่​ยก​มา​ใน​บท​นี้ ทั้ง​เงื่อนไข​ของ @EnableCaching คำ​อธิบาย​ของ starter และ​ตัว​ที่​ใช้ ConcurrentHashMap เป็น​ที่​เก็บ

  • ตัวเลข​และ​ข้อความ error ใน​บท​นี้​มา​จาก scripts/clinic-cache/probe.py ซึ่ง​สร้าง​สอง​ทรง​ที่ code Kotlin เท่า​กัน​ทุก byte แล้ว​เทียบ runtimeClasspath ที​ละ​ตัว · ต้อง​สั่ง scripts/clinic-cache/up.sh ก่อน ซึ่ง​ยก container ของ​คอร์ส​นี้​เอง​ขึ้น​ที่ port 55436

เช็กความเข้าใจ — บทที่ 2

ข้อ 1 / 3

ติด `@EnableCaching` กับ `@Cacheable` ครบแล้ว แต่ไม่แตะ build file ผลที่วัดได้คืออะไร