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

คำ​เรียก​ที่​เกิด​ใน class เดียวกัน ไม่​เคย​ไป​ถึง proxy

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

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

ทาง​เข้า​ใหม่​ที่​เรียก​เพื่อน​บ้าน​ของ​ตัว​เอง

หัวข้อ​ที่​มีชื่อ​ว่า “ทาง​เข้า​ใหม่​ที่​เรียก​เพื่อน​บ้าน​ของ​ตัว​เอง”

เพิ่ม method หนึ่ง​ตัว​ที่​เรียก visits() ตรงๆ จาก​ใน class เดียวกัน ไม่​แก้ visits() เลย

visits() ยัง​เป็น​ตัว​เดิม พร้อม​กุญแจ​ที่​บท​ที่ 3 ใส่​ให้ ส่วน​ของ​ใหม่​คือ​บรรทัด​ล่าง

VisitService — สอง method ที่​บท​นี้​วัด · ทรง​ที่ probe สร้าง​ยัง​มี evict() อยู่​ด้วย
@Service
class VisitService(private val repo: VisitRepo, private val tenant: CurrentTenant) {
@Cacheable("visits", key = "@currentTenant.id")
fun visits(): List<Visit> = repo.findByTenantId(tenant.id)
/** เรียกเพื่อนบ้านใน class เดียวกัน · คำเรียกนี้ไม่ผ่าน proxy */
fun visitsViaSelf(): List<Visit> = visits()
}

ปลาย​ทาง HTTP ของ​ทาง​ใหม่​นั้น​ก็​ตรง​ไป​ตรง​มา มัน​เรียก visitsViaSelf() แทน visits()

ปลาย​ทาง​ที่​ยิง​เข้าไป
@GetMapping("/visits/self")
fun viaSelf(): Map<String, Any> = shape(svc.visitsViaSelf())

อ่าน​สอง​บล็อก​ข้าง​บน​แล้ว​ไม่มี​อะไร​น่า​สงสัย visitsViaSelf() เรียก visits() และ visits() ติด @Cacheable ไว้​ครบ

ยิง /visits/self สอง​ครั้ง ฐาน​ข้อมูล​ถูก​ถาม 2 ครั้ง และ​กุญแจ​ใน cache ยัง​เป็น []

ช่อง​ที่​วัดหลัง​ยิง /visits/self สอง​ครั้งหลัง​ยิง /visits อีก​หนึ่ง​ครั้ง
ฐาน​ข้อมูล​ถูก​ถาม2 ครั้ง3 ครั้ง
กุญแจ​ใน cache[][clinic-a]

สอง​คำขอ สอง​ครั้ง​ที่​ลง​ไป​ถึง​ฐาน​ข้อมูล นั่น​คือ​ตัวเลข​ของ​แอป​ที่​ไม่มี cache เลย

ช่อง​ที่​สอง​บอก​มากกว่า​นั้น​อีก กุญแจ​ใน cache เป็น​รายการ​ว่าง แปล​ว่า​ไม่ใช่​แค่​ไม่​ได้​ค่า​เดิม​กลับ​มา แต่​ไม่​เคย​มี​ใคร​เขียน​อะไร​ลง​ไป​เลย​สัก​ใบ

นี่​ไม่ใช่ cache misscache missการ​ที่​กุญแจ​ของ​คำ​เรียก​ไม่มี​อยู่​ใน cache จึง​ต้อง​ลง​ไป​ถาม​ของ​จริง · การ​นับ miss คือ​วิธี​เดียว​ที่​บอก​ว่า​ชั้น​นี้​ทำงาน​อยู่ หรือ​ถูก​ข้าม​ไป​ทั้ง​ชั้น ที่​ตาม​ด้วย​การ​เติม​ค่า​กลับ ถ้า​เป็น miss จริง คำขอ​แรก​จะ​เขียน​กุญแจ​ไว้​ให้​คำขอ​ที่​สอง ที่​วัด​ได้​คือ​ชั้น​นี้​ถูก​ข้าม​ไป​ทั้ง​ชั้น

เปลี่ยน​ไป​ยิง /visits หนึ่ง​ครั้ง ฐาน​ข้อมูล​ถูก​ถาม​เพิ่ม​เป็น 3 และ​กุญแจ​ปรากฏ​ขึ้น​ใน cache

คำขอ​ที่​สาม​ยิง​เข้า /visits ซึ่ง​เป็น​ทาง​เดิม​ของ​บท​ก่อน​หน้า ผล​ที่​ได้​เปลี่ยน​สอง​ช่อง​พร้อม​กัน

  • ฐาน​ข้อมูล​ถูก​ถาม​เพิ่ม​เป็น 3 ครั้ง ซึ่ง​คาด​ไว้​อยู่​แล้ว เพราะ​ยัง​ไม่มี​ใคร​เก็บ​อะไร​ไว้​ให้
  • กุญแจ​ใน cache กลาย​เป็น [clinic-a] จาก​ที่​เป็น [] มา​ตลอด​สอง​คำขอ​แรก

ของ​สอง​อย่าง​นี้​ต่าง​กัน​แค่​ทาง​เข้า visits() ตัว​เดียวกัน กุญแจ​สูตร​เดียวกัน ผู้​เช่า​คน​เดียวกัน เข้า​ทาง​หนึ่ง​แล้ว​กลไก​ทำงาน เข้า​อีก​ทาง​แล้ว​ไม่​ทำงาน

Spring ห่อ bean ไว้​ด้วย proxy งาน​ของ annotation เกิด​ที่​ตัว​ห่อ คำ​เรียก​ที่​ไม่​ผ่าน​ตัว​ห่อ​จึง​ไม่มี​ใคร​ทำงาน​ให้

proxyproxyตัว​ห่อ bean ที่ Spring สร้าง​ขึ้น​เพื่อ​ทำงาน​ของ annotation ก่อน​และ​หลัง​คำ​เรียก​จริง · กลไก​ของ cache อยู่​ที่​ตัว​ห่อ ไม่​ได้​อยู่​ใน​ตัว method คือ bean ตัว​ที่​ผู้​อื่น​ถือ​อยู่​จริง เมื่อ VisitEndpoint เรียก svc.visits() มัน​เรียก​ตัว​ห่อ ตัว​ห่อ​เช็​กกุญ​แจ​ก่อน แล้ว​ค่อย​ตัดสิน​ว่า​จะ​เรียก​ของ​จริง​หรือ​ไม่​เรียก

แต่​ข้าง​ใน visitsViaSelf() คำ​ว่า visits() หมาย​ถึง this.visits() และ this คือ อ็อบเจ็กต์​ของ​จริง ไม่ใช่​ตัว​ห่อ คำ​เรียก​นั้น​จึง​เดิน​ตรง​เข้า​ตัว​งาน ไม่​ผ่าน​ด่าน​ไหน​เลย

นี่​คือ self-invocationself-invocationการ​ที่ method หนึ่ง​เรียก​อีก method ใน class เดียว​กัน​ตรงๆ · คำ​เรียก​นั้น​ไม่​ผ่าน proxy annotation จึง​ไม่มี​ผล · วัด​แล้ว​ว่า​เรียก​สอง​ครั้ง ฐาน​ข้อมูล​ถูก​ถาม​สอง​ครั้ง และ​กุญแจ​ใน cache ยัง​เป็น `[]` และ​เอกสาร​ของ Spring เขียน​ผล​ของ​มัน​ไว้​ตรงๆ — “self invocation is not going to result in the advice associated with a method invocation getting a chance to run”

flowchart TB
  A["คำขอ /visits/self"] --> P["proxy ที่ห่อ VisitService ไว้"]
  B["คำขอ /visits"] --> P
  P -->|"เข้าทาง visitsViaSelf() · ไม่มี annotation ให้ทำงาน"| S["visitsViaSelf()"]
  S -->|"เรียกเพื่อนบ้านในตัวเดียวกัน · ไม่ย้อนออกไปที่ proxy"| V["visits()"]
  P -->|"เข้าทาง visits() · ด่านของ @Cacheable อยู่ตรงนี้"| K{"กุญแจใบนี้อยู่ใน cache แล้วหรือยัง"}
  K -->|"ยังไม่มี · จึงเขียนกุญแจไว้หลังได้คำตอบ"| V
  V --> DB[("ฐานข้อมูล")]

คำ​บรรยาย​ภาพ: ทั้ง​สอง​ทาง​ลงท้าย​ที่ visits() ตัว​เดียวกัน แต่​ทาง​บน​เข้า​ถึง​ตัว​งาน​โดย​ไม่​ผ่าน​ด่าน ของ @Cacheable ส่วน​ทาง​ล่าง​ผ่าน​ด่าน​นั้น​ก่อน จึง​มี​กุญแจ​เกิด​ขึ้น​เฉพาะ​ทาง​ล่าง

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

รวบ​สิ่ง​ที่​ผู้​ดูแล​ระบบ​มอง​เห็น​ใน​ทรง​นี้

  • คำ​ตอบ​ถูก​ต้อง​ทุก​แถว ผู้​เรียก​ได้​แถว​ของ​ผู้​เช่า​ตัว​เอง​ครบ
  • รหัส​ที่​คืน​คือ​รหัส​สำเร็จ ไม่มี​รหัส​ข้อ​ผิด​พลาด​สัก​ตัว
  • ไม่มี​ข้อความ​เตือน​ว่า​มี annotation ตัว​ไหน​ถูก​ข้าม

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

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

มี​สอง​ทาง​ที่​ทำให้​คำ​เรียก​วิ่ง​ผ่าน​ตัว​ห่อ​จริง และ​ทั้ง​สอง​ทาง​เก็บ​ราคา​คนละ​แบบ

probe ของ​คอร์ส​นี้​วัด​แต่​ทรง​ที่​พัง​ไว้ ไม่​ได้​วัด​ทรง​ที่​แก้​แล้ว ส่วน​ข้าง​ล่าง​จึง​บอก​ทิศทาง ไม่​ได้​บอก​ตัวเลข

  • ย้าย​ผู้​เรียก​ออก​ไป​อยู่​คนละ bean คำ​เรียก​กลาย​เป็น​คำ​เรียก​ข้าม bean จึง​ผ่าน proxy ตาม​ปกติ ราคา​คือ​มี bean เพิ่ม​มา​อีก​ตัว และ​เส้น​แบ่ง​หน้าที่​ระหว่าง​สอง​ตัว​นั้น​ต้อง​อธิบาย​ให้​ได้
  • ฉีด​ตัว​เอง​กลับ​เข้า​มา​แล้ว​เรียก​ผ่าน​ตัว​ที่​ฉีด คำ​เรียก​จะ​ไป​ที่​ตัว​ห่อ ราคา​คือ bean ตัว​นั้น​ถือ​อ้างอิง​ที่​ชี้​กลับ​มา​หา​ตัว​เอง ซึ่ง​อ่าน​แล้ว​สะดุด และ​คน​ที่​แก้​ต่อ​มัก​ลบ​ทิ้ง​เพราะ​ดู​เหมือน​ของ​เกิน

ทาง​ที่​แพง​ที่สุด​คือ​ไม่​ทำ​อะไร แล้ว​ปล่อย​ให้​ตัวเลข​ที่​ไม่​ลด​กลาย​เป็น​ข้อ​สรุป​ว่า​เครื่อง​มือ​ใช้​ไม่​ได้

บท​นี้​พูด​ถึง​คำ​เรียก​ที่​ไป​ไม่​ถึง cache ส่วน​บท​หน้า​พูด​ถึง​ค่าที่​ไป​ถึง​แล้ว และ​ถูก​คน​ข้าง​นอก​แก้

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

บท​ที่ 5 วัด​ข้อ​นั้น


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • Spring Framework — Proxying Mechanisms (เข้า​ถึง 2026-09-04) — ที่มา​ของ​ประโยค​เรื่อง self invocation กับ advice ที่​ไม่​ได้​รัน

  • ตัวเลข​ทุก​ตัว​ใน​บท​นี้​มา​จาก scripts/clinic-cache/probe.py ซึ่ง​ยิง​คำขอ​จริง​แล้ว​นับ query ที่​ลง​ถึง​ฐาน​ข้อมูล · ต้อง​สั่ง scripts/clinic-cache/up.sh ก่อน ซึ่ง​ยก container ของ​คอร์ส​นี้​เอง​ขึ้น​ที่ port 55436

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

ข้อ 1 / 3

ยิง `/visits/self` สองครั้ง ซึ่งเข้าทาง `visitsViaSelf()` ผลที่วัดได้คืออะไร