ตัวจัดการที่เขียนไว้ แล้วไม่เคยได้ทำงานเลย
สวิตช์ในบทที่แล้วเปลี่ยนซองจดหมายให้ · ตอนนี้ถึงตาเราเขียนข้อความข้างในเอง
ระบบรู้อยู่แล้วว่าช่องไหนผิดเพราะอะไร · สิ่งที่ขาดคือคนที่หยิบความรู้นั้นใส่ลงใน body
เขียนตัวจัดการตัวแรก
หัวข้อที่มีชื่อว่า “เขียนตัวจัดการตัวแรก”ดักชนิดเดียว แล้วเติมคีย์ของตัวเองเข้าไปในรูปมาตรฐาน
package clinic.http
import org.springframework.http.HttpStatusimport org.springframework.http.ProblemDetailimport org.springframework.web.bind.MethodArgumentNotValidExceptionimport org.springframework.web.bind.annotation.ExceptionHandlerimport org.springframework.web.bind.annotation.RestControllerAdvice
@RestControllerAdviceclass 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 เขียว · โปรแกรมสตาร์ตขึ้น · ไม่มีคำเตือนสักบรรทัด
แล้วยิงคำขอที่ผิดสามช่องเข้าไป
หัวข้อที่มีชื่อว่า “แล้วยิงคำขอที่ผิดสามช่องเข้าไป”นี่คือจุดที่บทนี้มีอยู่เพื่อมัน
{"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
package clinic.http
import org.springframework.core.annotation.Orderimport org.springframework.http.HttpStatusimport org.springframework.http.ProblemDetailimport org.springframework.web.bind.MethodArgumentNotValidExceptionimport org.springframework.web.bind.annotation.ExceptionHandlerimport org.springframework.web.bind.annotation.RestControllerAdvice
@Order(-1)@RestControllerAdviceclass 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 }}ยิงคำขอเดิมเข้าไปใหม่
{"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 | ผู้เรียกรู้ว่าช่องไหนผิดไหม |
|---|---|---|---|
| ค่าตั้งต้น | 400 | application/json | ไม่ |
| เปิด problemdetails | 400 | application/problem+json | ยังไม่ |
| เพิ่ม advice ที่ไม่ระบุลำดับ | 400 | application/problem+json | ยังไม่ · advice ไม่ได้รันเลย |
ใส่ @Order(-1) ให้ advice นั้น | 400 | application/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 ตัวแรกไม่ได้รัน