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

ตัว​จัดการ​ที่​เขียน​ไว้ แล้ว​ไม่​เคย​ได้​ทำงาน​เลย

สวิตช์​ใน​บท​ที่​แล้ว​เปลี่ยน​ซอง​จดหมาย​ให้ · ตอน​นี้​ถึง​ตา​เรา​เขียน​ข้อความ​ข้าง​ใน​เอง

ระบบ​รู้​อยู่​แล้ว​ว่า​ช่อง​ไหน​ผิด​เพราะ​อะไร · สิ่ง​ที่​ขาด​คือ​คน​ที่​หยิบ​ความ​รู้​นั้น​ใส่​ลง​ใน body

ดัก​ชนิด​เดียว แล้ว​เติม​คีย์​ของ​ตัว​เอง​เข้าไป​ใน​รูป​มาตรฐาน

app/src/main/kotlin/clinic/http/ValidationAdvice.kt
package clinic.http
import org.springframework.http.HttpStatus
import org.springframework.http.ProblemDetail
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
@RestControllerAdvice
class ValidationAdvice {
@ExceptionHandler(MethodArgumentNotValidException::class)
fun onInvalid(e: MethodArgumentNotValidException): ProblemDetail {
val body = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST)
body.title = "KAEN-ADVICE-RAN"
body.detail = "คำขอมีช่องที่กรอกไม่ถูกต้อง"
body.setProperty(
"errors",
e.bindingResult.fieldErrors.associate { it.field to (it.defaultMessage ?: "") },
)
return body
}
}

@ExceptionHandler@ExceptionHandlerการ​ผูก​ตัว​จัดการ​หนึ่ง​ตัว​เข้า​กับ​ชนิด​ของ exception หนึ่ง​ชนิด · ผูก​ถูก​ชนิด​แล้ว​ยัง​ไม่​พอ ถ้า​มี​ตัว​อื่น​ที่​ผูก​ชนิด​เดียวกัน​และ​มา​ก่อน คือ​สิ่ง​ที่​ผูก​ตัว​จัดการ​ตัว​นี้​เข้า​กับ exception ชนิด​เดียว · ชนิด​อื่น​ที่​หลุด​ออก​มา​จะ​ไม่​ผ่าน​ตัว​นี้​เลย

title ที่​เขียน​ไว้​ไม่ใช่​ข้อความ​สำหรับ​ผู้​ใช้ · มัน​คือ markermarkerค่าที่​ไม่มี​ทาง​บังเอิญ​ตรง ซึ่ง​ใส่​ไว้​เพื่อ​พิสูจน์​ว่า​สิ่ง​ที่​เรา​เขียน​ได้​ทำงาน​จริง · ถาม​ว่า​ใคร​ตอบ ไม่ใช่​ถาม​ว่า​มี​คน​ตอบ​ไหม · รหัส​ตอบ​กลับ​ที่​ถูก​ต้อง​พิสูจน์​ข้อ​นี้​ไม่​ได้ · เรา​จะ​อธิบาย​เหตุผล​ของ​มัน​ใน​อีก​สอง​หัวข้อ​ข้าง​ล่าง

build เขียว · โปร​แก​รม​ส​ตาร์ต​ขึ้น · ไม่มี​คำ​เตือน​สัก​บรรทัด

นี่​คือ​จุด​ที่​บท​นี้​มี​อยู่​เพื่อ​มัน

ตอบ​กลับ · 400 · application/problem+json
{"detail":"Invalid request content.","instance":"/bookings","status":400,"title":"Bad Request"}

สี่​คีย์ · ไม่มี​คีย์ errors · title เป็น Bad Request

ตัว​จัดการ​ที่​เรา​เพิ่ง​เขียน ไม่​ได้​ทำงาน​เลย​สัก​ครั้ง

เพราะ​รหัส 400 พิสูจน์​ข้อ​นี้​ไม่​ได้ และ body ก็​พิสูจน์​ไม่​ได้ ถ้า​ไม่มี marker

ถ้า​ตัว​จัดการ​ของ​เรา​ทำงาน มัน​ก็​ตอบ 400 · ถ้า​มัน​ไม่​ทำงาน Spring ก็​ตอบ 400 อยู่ดี · รหัส​ตอบ​กลับ​แยก​สอง​กรณี​นี้​ไม่​ออก

marker คือ​ค่าที่​ไม่มี​ทาง​บังเอิญ​ตรง ซึ่ง​เรา​วาง​ไว้​ใน​ทาง​ที่​เรา​สงสัย · ถ้า​มัน​โผล่​ออก​มา แปล​ว่า​ทาง​นั้น​ถูก​เดิน · ถ้า​ไม่​โผล่ แปล​ว่า​ไม่​ถูก​เดิน

KAEN-ADVICE-RAN ไม่มี​ทาง​เป็น​ค่าที่ Spring คิด​ขึ้น​เอง · การ​ที่​มัน​ไม่​โผล่ จึง​เป็น​หลัก​ฐาน ไม่ใช่​ข้อ​สันนิษฐาน

🎯 ถาม​ว่า​ใคร​ตอบ ไม่ใช่​ถาม​ว่า​มี​คน​ตอบ​ไหม

คำถาม​ที่​ตอบ​ไม่​ได้​ด้วย​รหัส​ตอบ​กลับ มี​มากกว่า​ที่​คน​ส่วน​ใหญ่​คิด

คำถาม​ที่​ถาม​ว่า “ทำงาน​ไหม” ต้องการ​ค่าที่​ตอบ​ได้​แค่​ทาง​เดียว · ไม่ใช่​ค่าที่​ทั้ง​สอง​ทาง บังเอิญ​ให้​เหมือน​กัน

หลัก​เดียวกัน​นี้​ใช้​กับ​การ​วัด​ทุก​อย่าง​ใน​คอร์ส​นี้ · probe ของ​คอร์ส​จึง​บังคับ​ให้ marker ต้อง​โผล่​ใน​ทรง​ที่​ควร​โผล่ และ​ต้อง​ไม่​โผล่​ใน​ทรง​ที่​ไม่​ควร

@RestControllerAdvice ที่​ไม่​ระบุ​ลำดับ ได้​ลำดับ​ต่ำ​สุด

Spring มี​ตัว​จัดการ​ของ​ตัว​เอง​สำหรับ MethodArgumentNotValidExceptionMethodArgumentNotValidExceptionexception ที่​เกิด​เมื่อ `@Valid` ไม่​ผ่าน · Spring มี​ตัว​จัดการ​ของ​ตัว​เอง​สำหรับ​ชนิด​นี้​อยู่​แล้ว ซึ่ง​เป็น​เหตุผล​เดียว​ที่​ลำดับ​สำคัญ​ใน​บท​นี้ อยู่​แล้ว · นั่น​คือ​ตัว​ที่​ตอบ Invalid request content. มา​ตั้งแต่​บท​ที่ 2

เมื่อ​มี​สอง​ตัว​รับ​ชนิด​เดียวกัน Spring เรียง​ตาม @Order@Orderลำดับ​ของ​ตัว​จัดการ​เมื่อ​มี​หลาย​ตัว​รับ​ชนิด​เดียวกัน · ตัว​ที่​ไม่​ประกาศ​ลำดับ​ได้​ลำดับ​ต่ำ​สุด จึง​แพ้​ตัว​จัดการ​ของ Spring ที่​อยู่​ที่​ศูนย์ · **ไม่มี​อะไร​เตือน​เลย** ทั้ง​ตอน​แปล​และ​ตอน​รัน แล้ว​เลือก​ตัว​แรก​ที่​เจอ · ตัว​ของ Spring อยู่​ที่​ลำดับ 0 · ตัว​ของ​เรา​ที่​ไม่​ได้​ประกาศ​อะไร​เลย ได้​ลำดับ​ต่ำ​สุด

ไม่มี​อะไร​บอก​เรา​ว่า​มี​ตัว​อื่น​แข่ง​อยู่ · ไม่มี​คำ​เตือน​ตอน​แปล ไม่มี log ตอน​ส​ตาร์ต ไม่มี​อะไร​ตอน​รัน

ประกาศ​ลำดับ​ให้​ต่ำ​กว่า​ของ Spring

app/src/main/kotlin/clinic/http/ValidationAdvice.kt
package clinic.http
import org.springframework.core.annotation.Order
import org.springframework.http.HttpStatus
import org.springframework.http.ProblemDetail
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
@Order(-1)
@RestControllerAdvice
class ValidationAdvice {
@ExceptionHandler(MethodArgumentNotValidException::class)
fun onInvalid(e: MethodArgumentNotValidException): ProblemDetail {
val body = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST)
body.title = "KAEN-ADVICE-RAN"
body.detail = "คำขอมีช่องที่กรอกไม่ถูกต้อง"
body.setProperty(
"errors",
e.bindingResult.fieldErrors.associate { it.field to (it.defaultMessage ?: "") },
)
return body
}
}

ยิง​คำขอ​เดิม​เข้าไป​ใหม่

ตอบ​กลับ · 400 · application/problem+json
{"detail":"คำขอมีช่องที่กรอกไม่ถูกต้อง","errors":{"doctorId":"must not be blank","endMinute":"must be greater than or equal to 1","startMinute":"must be greater than or equal to 0"},"instance":"/bookings","status":400,"title":"KAEN-ADVICE-RAN"}

marker โผล่ · คีย์ errors โผล่ · และ​มัน​บอก​ครบ​ทั้ง​สาม​ช่อง​พร้อม​เหตุผล​ของ​แต่ละ​ช่อง

ตาราง​นี้​คือ​สาม​บท​แรก​ของ​คอร์ส​ทั้งหมด

ทรงรหัสcontent-typeผู้​เรียก​รู้​ว่า​ช่อง​ไหน​ผิด​ไหม
ค่า​ตั้ง​ต้น400application/jsonไม่
เปิด problemdetails400application/problem+jsonยัง​ไม่
เพิ่ม advice ที่​ไม่​ระบุ​ลำดับ400application/problem+jsonยัง​ไม่ · advice ไม่​ได้​รัน​เลย
ใส่ @Order(-1) ให้ advice นั้น400application/problem+jsonใช่ · ครบ​สาม​ช่อง

สี่​แถว​นี้​ให้​รหัส​เดียวกัน​หมด · ถ้า​เรา​วัด​ด้วย​รหัส​อย่าง​เดียว เรา​จะ​สรุป​ว่า​ไม่มี​อะไร เปลี่ยน​เลย​ตั้งแต่​แถว​แรก

⚠️ ลำดับ​สำคัญ​เฉพาะ​ตอน​ที่​มี​คน​แข่ง

@Order(-1) ไม่ใช่​ของ​ที่​ต้อง​ใส่​ทุก​ครั้ง​ที่​เขียน advice · มัน​จำเป็น​ตรง​นี้​เพราะ Spring มี​ตัว​จัดการ​ของ​ตัว​เอง​สำหรับ​ชนิด​นี้​อยู่​แล้ว

บท​ที่ 4 จะ​เขียน advice อีก​ตัว​ที่​ไม่​ต้อง​ใส่ @Order เลย และ​มัน​ทำงาน​ได้​ทันที เพราะ​ไม่มี​ใคร​แข่ง · ความ​ต่าง​ของ​สอง​กรณี​นี้​คือ​สิ่ง​ที่​ต้อง​เข้าใจ ไม่ใช่​กฎ​ที่​ต้อง​ท่อง

สาม​ข้อ · และ​ข้อ​ที่​สอง​คือ​ข้อ​ที่​คน​พลาด​กัน​มาก​ที่สุด

  • advice ที่​ไม่​ระบุ​ลำดับ ไม่​ได้​รัน​เลย พิสูจน์​ด้วย marker ที่​ไม่​โผล่​ใน body
  • รหัส​ตอบ​กลับ​พิสูจน์​เรื่อง​นี้​ไม่​ได้ ทั้ง​สี่​ทรง​ตอบ 400 เหมือน​กัน​หมด
  • ใส่ @Order(-1) แล้ว​ได้​คีย์ errors ครบ​สาม​ช่อง พร้อม​เหตุผล​ของ​แต่ละ​ช่อง

บท​ที่ 4 จะ​ย้าย​ไป​ที่​ความ​ล้ม​เหลว​อีก​ชนิด ซึ่ง​บท​ที่ 2 พิสูจน์​ไว้​แล้ว​ว่า สวิตช์​ปกติ​ครอบ​ไม่​ถึง

🔬 ตัวเลข​ทุก​ตัว​ใน​บท​นี้​มา​จาก​ไหน
  • สอง​ทรง​ใน​บท​นี้​ต่าง​กัน​ที่​บรรทัด @Order(-1) กับ import ของ​มัน​เท่านั้น · probe มี​ข้อ​ยืนยัน​ที่​แดง​ถ้า​สอง source นี้​ต่าง​กัน​เกิน​กว่า​นั้น
  • marker KAEN-ADVICE-RAN เป็น​ตัว​เดียว​กับ​ที่ npm run verify:clinic-http ฝัง​ไว้
  • Spring Boot 4.1.1 · Kotlin 2.4.10 · JDK 25
  • Spring Framework — Controller Advice (เข้า​ถึง 2026-09-02) — การ​เรียง​ลำดับ​ของ advice หลาย​ตัว
  • Ordered.LOWEST_PRECEDENCE (เข้า​ถึง 2026-09-02) — ค่าที่ bean ได้​เมื่อ​ไม่​ประกาศ​ลำดับ

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

ข้อ 1 / 3

อะไรเป็นหลักฐานว่า advice ตัวแรกไม่ได้รัน