@RequestMapping 省掉的不是幾行 XML,是你腦子裡那張請求分派表
要讓 /users 這個網址對到一段處理程式,Servlet 時代你得先開一個 class 繼承 HttpServlet,覆寫 doGet,再翻到 web.xml 補上兩段 XML:一段 <servlet> 宣告這個 class,一段 <servlet-mapping> 把它綁到 /users。兩段分開寫,中間靠一個 <servlet-name> 字串對上。字串打錯,編譯器不會理你,Tomcat 起來之前你也不會知道,等到瀏覽器噴 404 才回頭找,原來是 mapping 裡的名字少打一個字。
同樣一件事,現在你只要在方法上面掛一行 @GetMapping("/users")。
大部分人講到這裡就結束了,結論是「註解比較簡潔,少打很多字」。這句話沒錯,但它把重點講小了。註解省下的不是那幾行 XML,是那張「哪個請求該交給哪段程式」的分派表本身。這張表以前存在你腦子裡,也散在每個 doGet 的 if-else 裡。現在它被搬走了。這篇就順著同一個動作,一段一段對照,看那張表是怎麼從你手上消失的。
掛一個網址上去,以前要動三個地方
Servlet 的世界裡,一個端點的身分證分散在三處。class 本身、web.xml 的宣告、web.xml 的對應。
1 | <!-- web.xml:光註冊一個端點就要這兩段 --> |
1 | // 然後才是真正做事的 class |
我維護過一個 web.xml 快 800 行的老專案。光是想知道某個網址對到哪支程式,就得先 Ctrl-F 找 url-pattern,抄下 servlet-name,再 Ctrl-F 一次 servlet-name,才連到 servlet-class。三跳。而且每加一個端點,這套動作重來一遍。端點多起來之後,web.xml 自己就變成一個要維護的怪物。
換到 Spring MVC,這三處收成一處:
1 |
|
網址、HTTP 方法、要跑的程式,全部黏在同一個地方。改路徑就改這一行,沒有第二個檔案會跟你的記憶不同步。
GET 還是 POST,參數又是怎麼進來的
第二個對照更能看出差別。以前你在 Servlet 裡分辨請求是 GET 還是 POST,靠的是覆寫不同的方法名:doGet、doPost、doPut。方法名就是你的分岔器。
拿參數更痛。request.getParameter("id") 回給你的永遠是 String,而且可能是 null。所以每支程式的開頭都長一個樣:
1 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) { |
前面五六行全是雜訊,取值、判空、轉型、擋錯。每支程式都抄一遍,抄到你懷疑人生。
Spring MVC 把這一段吃掉了:
1 |
|
@PathVariable 從路徑挖值,@RequestParam 從 query string 挖值,型別轉換框架幫你做,缺值給不給預設你自己說了算。你寫的第一行就是業務邏輯,不是擋參數的樣板。這個轉變很多人無感,因為他們沒手寫過那五六行。寫過的人第一次用到 @RequestParam,只會講一句:真香。
同一個網址,想分岔怎麼辦
最能秀肌肉的地方在這。同一條路徑 /users,你可能想讓「送 JSON 的請求」和「送表單的請求」走不同的方法,或者依 header 分流給不同版本的 API。
Servlet 時代這種分岔只能寫在程式裡:
1 | protected void doPost(HttpServletRequest req, HttpServletResponse resp) { |
分岔的邏輯藏在方法內部。你不點進去讀,永遠不知道這個端點原來會依 content type 走兩條路。
@RequestMapping 把這些分岔條件全部提到方法簽名外面,變成可以宣告的屬性:
1 |
|
consumes 管「我只收這種 content type」,produces 管「我回這種」,還有 params、headers 可以要求某個參數存在、某個 header 等於某值。路徑一樣,條件不同,Spring 自己會挑對的方法。分岔的規則從「藏在程式裡的 if」變成「寫在方法門口的標籤」,你掃一眼註解就知道這個端點吃什麼、吐什麼。
註解不是魔法,框架只是幫你把那張表建好了
講到這裡該停下來問一句:這些標籤到底怎麼生效的?如果你以為 Spring 每次收到請求都去掃一遍所有註解,那它早就慢到炸了。
真相樸素很多。Spring MVC 前面站著一個 DispatcherServlet,它其實就是那個 Servlet 時代你要自己註冊的東西,只是這次全站只有一個,所有請求都先進它手裡。應用程式啟動時,框架做一件事:把所有 @Controller 掃過一遍,把每個方法上的 @RequestMapping 資訊抽出來,建成一張對照表,存在一個叫 RequestMappingHandlerMapping 的元件裡。表的 key 是「路徑加上那些條件」,value 是「該呼叫哪個方法」。
之後每個請求進來,DispatcherServlet 拿請求的路徑、方法、header、content type 去查這張表,找到對應的 handler method,準備好參數,呼叫它。就這樣。
這在設計上有個名字,叫 Front Controller。所有流量走同一個門,門後掛一張路由表。你會發現,這跟 web.xml 那套查表在本質上是同一件事:都是「路徑 → 程式」的對照。差別只在誰來維護這張表。以前是你,用手在 XML 和 if-else 裡維護;現在是框架,在啟動那一刻掃註解自動建好。註解從來不是魔法,它只是把你本來手寫的分派表,改成用貼標籤的方式讓框架幫你組。
這套自由,也是有代價的
我不想把它講得只有好處。端點散落在各個 Controller,這件事反過來也是缺點:沒有任何一個檔案能讓你一眼看完全站有哪些網址。web.xml 至少醜歸醜,全部都在那一頁。現在你想看全貌,得靠 Spring Boot Actuator 的 /mappings 端點,或是 IDE 幫你列。
還有一個隱藏成本。加端點變得太容易,容易到大家隨手就加,結果同一個專案裡 /getUser、/user/query、/users 三種命名風格並存,沒人統一。工具把摩擦降到零的時候,紀律就得由你自己補上。註解的條件也一樣,params、headers、consumes 疊太多,路由會變得比 if-else 還難讀。方便和隱晦,常常是同一枚硬幣的兩面。
真正被換掉的東西
回到開頭那張分派表。
Servlet 的路由是命令式的:你寫程式,一步步告訴機器「先看路徑、再看方法、然後 if 這個 else 那個」,分派的流程是你親手跑的。Spring MVC 的路由是宣告式的:你只描述「這個方法負責 POST 的 /users、只收 JSON」,至於怎麼從一大堆請求裡把對的那個送過來,交給框架。
差別不在少打幾行 XML,在於「這個請求該給誰」這個決策,從每次請求都要跑一遍的 runtime if-else,被搬到了啟動時建好、之後只查不改的一張表。你腦子裡不用再存那張表,程式裡也不用再跑那套分岔。
所以下次有人問你 @RequestMapping 好在哪,別只回「比較簡潔」。它真正做的事,是把路由這件事從「你要親自分派的動作」變成「你只要描述的事實」。能被描述清楚、交出去的東西,就不必再由你每次親手做一遍。這才是那一行註解換掉的東西。










