本节摘要:分发器如何知道某个路径归哪个方法?答案是 HandlerMapping 维护的路由表,它在启动期建立、运行期只查。本节讲路由表的构建规则、路径匹配的优先级陷阱,以及拦截器的三个回调时机与典型工程用法。
应用启动时,Spring 扫描所有控制器,把每个 @GetMapping、@PostMapping 注解里的路径抽取出来,与方法、参数、所在 Bean 一起注册成一条映射记录。请求到来时按"精确优先、通配靠后"的规则查表。两个控制器如果声明了完全相同的路径,启动直接失败并打印冲突详情——这是框架在保护你,别怕红字,怕的是没红字。
看一个稍微复杂的注册与匹配:
@RestController @RequestMapping("/api/users") public class UserController { @GetMapping("/{id}") // 匹配 /api/users/42 public UserVO one(@PathVariable Long id) { ... } @GetMapping("/me") // 匹配 /api/users/me public UserVO me() { ... } }
两条规则有重叠:me 也能被 {id} 捕获。框架的裁定是精确路径胜出——查路径为 me 的请求走第二个方法,42 才走第一个。这条规则大多数时候符合直觉,但多个通配层级叠加时(列表页、详情页、子资源混在一个控制器),建议画出路由表再核对,肉眼排序最容易错。
⚠️ 尾斜杠陷阱:
/users/42与/users/42/在新版本默认不是同一地址,后者的匹配会落空返回 404。网关改写路径时尤其要检查这一格差异。
路由确定后、方法调用前后,拦截器获得三次介入机会。注册一个用于接口计数的拦截器:
@Component public class ApiMetricInterceptor implements HandlerInterceptor { private static final Logger log = LoggerFactory.getLogger(ApiMetricInterceptor.class); @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) { req.setAttribute("t0", System.nanoTime()); return true; // false 则旅程止步于此 } @Override public void postHandle(HttpServletRequest req, HttpServletResponse resp, Object handler, ModelAndView mv) { log.info("业务耗时 {} 微秒", (System.nanoTime() - (Long) req.getAttribute("t0")) / 1000); } @Override public void afterCompletion(HttpServletRequest req, HttpServletResponse resp, Object handler, Exception ex) { if (ex != null) log.warn("请求以异常收尾", ex); } }
再把它挂到路径上:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new ApiMetricInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/ping"); } }
三个时机的分工很清楚:preHandle 做准入判断,返回假即终止;postHandle 在方法成功后、响应写出前介入,可以改视图模型但改不了已提交的响应;afterCompletion 一定执行(含异常路径),适合清理与统计。资源清理只放在 afterCompletion,放在 postHandle 里异常路径会漏。

背景:运营活动前担心某查询接口被打爆,想在业务方法之外加一道每秒请求数的粗筛。这正好落在拦截器的准入回调上。操作:用一个计数器数组实现固定窗口限流:
@Component public class RateLimitInterceptor implements HandlerInterceptor { private final AtomicLongArray counters = new AtomicLongArray(2); private volatile int window = 0; // 0 或 1 两个窗口交替 private static final long LIMIT = 500; // 每窗口五百次 @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws IOException { long now = System.currentTimeMillis() / 1000; // 秒级窗口 int slot = (int) (now % 2); if (slot != window) { counters.set(slot, 0); window = slot; } // 进入新窗口清零 if (counters.incrementAndGet(slot) > LIMIT) { resp.setStatus(429); // 请求过多 resp.getWriter().write("稍后再试"); return false; // 不交棒,旅程止步 } return true; } }
注册到目标路径后用压测工具连发六百个请求:结果约五百个正常返回,其余收到 429。解读:限流发生在拦截器准入回调,控制器与数据库毫发无损——这正是"粗筛放在方法之外"的收益。变式:把限流逻辑挪到过滤器里也能工作,但拦截器能拿到目标方法信息,未来可以做"按注解限流"——给重点接口标注解,拦截器读注解决定限额,过滤器则拿不到方法粒度的信息。这一对比也回答了 2.1 节的选型问题。
工程提示:生产环境用现成的限流组件而非手写,本例的价值是让你看懂拦截器准入回调的语义:拒绝时写响应并返回假,放行时什么都不写只返回真。