本节摘要:以一次"查询订单"的 GET 请求为样本,逐站跟踪它从浏览器到数据库再返回的全程。理解每一站的职责,是理解 Spring 全部机制的前提——后面的章节不过是把这里的每一站放大细讲。
用户在页面点击"我的订单",浏览器向服务端发出一个 GET 请求。这个请求在到达你写的第一行业务代码之前,其实已经穿过了好几层你从未直接打交道的组件。先给一个能亲手验证的最小样本:
@RestController public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService = orderService; } @GetMapping("/orders/{id}") public OrderVO detail(@PathVariable Long id) { return orderService.loadOrder(id); } }
代码只有几行,但请求的旅程远不止这几行。下面逐站拆开。
第一站:连接器。 内嵌的 Tomcat 监听端口,接受 TCP 连接,解析出 HTTP 报文,包装成 Servlet 规范的请求对象。你的代码感知不到它,但线程就是在这里被分配的——每个请求占用容器线程池里的一个线程,这是后面理解同步阻塞与异步处理的钥匙。
第二站:过滤器链。 一组按顺序执行的过滤器先于所有 Servlet 运行,负责编码设置、跨域处理、安全认证等"进门检查"。Spring Security 的整套防线就架在这一站。
第三站:DispatcherServlet。 Spring MVC 的总入口,一个前端控制器。它不干活,只负责调度:把请求交给谁、结果怎么处理,都由它统筹。
第四站:处理器映射。 DispatcherServlet 问 HandlerMapping:路径是订单详情、方法是 GET,哪个方法处理?答案就是上面代码里带 @GetMapping 注解的 detail 方法。
第五站:拦截器。 找到目标方法前后,注册的 HandlerInterceptor 依次执行前置处理,可以打日志、校验令牌,甚至直接拒绝请求。
第六站:参数绑定与控制器。 路径里的订单号被转成 Long 塞进方法参数,detail 方法开始执行。控制器应当只做"翻译":把 HTTP 概念翻译成业务调用,不写业务逻辑。
第七站:切面与事务。 orderService.loadOrder 的调用其实发生在代理对象上,日志切面、事务边界都包在方法外侧。第 4 章会看到这层"包装纸"如何生成。
第八站:数据访问层。 Service 调用 Repository,SQL 发往数据库,结果集映射回对象。
第九站:响应写出。 返回值经消息转换器序列化为 JSON,拦截器执行后置处理,容器把响应写回连接,线程归还线程池。

不必相信书本,两分钟能亲眼看到。开启 MVC 的调试日志:
logging: level: org.springframework.web: DEBUG
再发一次请求,控制台会依次打出映射查找、拦截器执行、控制器调用的日志,顺序与上面的九站一致。更直接的办法是在 detail 方法上打断点,调用栈从 Tomcat 的线程入口一直到你的方法,栈帧本身就是一张旅程地图——把调用栈读熟,比背十篇架构文章都有用。
⚠️ 常见误区:把控制器当业务层用。旅程的分工是有原因的——控制器站在 HTTP 与业务的交界上,塞进业务逻辑后,事务、切面、测试都会变得别扭。
光看日志还不够直观,下面这个实验能把九站旅程一次性"点亮"。背景:我们想知道每个环节的实际耗时与顺序,为后续章节的实验打好地基。操作分三步。
第一步,写一个最简单的拦截器,在三个回调里各打一行日志:
@Component public class JourneyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) { System.out.println("[拦截器-前置] " + req.getRequestURI()); return true; // 返回 false 旅程在此终止 } @Override public void postHandle(HttpServletRequest req, HttpServletResponse resp, Object handler, ModelAndView mv) { System.out.println("[拦截器-后置] 控制器已返回"); } @Override public void afterCompletion(HttpServletRequest req, HttpServletResponse resp, Object handler, Exception ex) { System.out.println("[拦截器-完成] 响应已写出"); } }
第二步,注册它,并顺手注册一个过滤器:
@Configuration public class WebConfig implements WebMvcConfigurer { private final JourneyInterceptor journeyInterceptor; WebConfig(JourneyInterceptor journeyInterceptor) { this.journeyInterceptor = journeyInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(journeyInterceptor).addPathPatterns("/**"); } @Bean public FilterRegistrationBean<Filter> timingFilter() { FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>(); bean.setFilter((request, response, chain) -> { System.out.println("[过滤器] 最外层进门检查"); chain.doFilter(request, response); // 不调用这句,后面全部短路 }); bean.setOrder(1); return bean; } }
第三步,再在 OrderService.loadOrder 的第一行打一行日志。发起请求后,控制台输出依次是:过滤器 → 拦截器前置 → Service 日志 → 拦截器后置 → 拦截器完成。结果解读:过滤器先于拦截器,说明它站在旅程更外层;拦截器完成回调在响应写出之后,说明它连异常路径也会走一遍。变式:把前置回调的返回值改成 false 再发请求,Service 日志消失,直接得到响应——这就是拦截器"半路拒绝"的形态,第 2 章会展开讲它的典型用途(登录校验、接口限流)。