单体架构深度剖析:典型单体架构(Spring MVC)的组件与通信方式

好,咱们接着聊。上一章我们讲了单体架构的整体面貌,这一章,我打算把Spring MVC这个经典的单体架构拿出来,拆开揉碎了给你看。

为什么选Spring MVC?因为它是Java生态里最典型的代表。我这些年见过的单体项目,十有八九都是它。说白了,你搞懂了Spring MVC,就搞懂了绝大多数单体架构的套路。

Spring MVC的核心组件

Spring MVC的架构,其实就围绕几个核心角色转。我习惯把它们分成三层:前端控制器、处理器映射器、视图解析器。嗯,还有模型数据,咱们一个一个说。

1. DispatcherServlet(前端控制器)

这是整个Spring MVC的入口。所有的HTTP请求,第一站就是它。

我记得刚接触Spring MVC时,总觉得这玩意儿像个“二传手”——自己不干活,专门把请求分发给别人。后来才明白,这正是它的高明之处。

它的工作流程很简单:

  • 接收请求
  • 找合适的处理器(Controller)
  • 调用处理器
  • 处理返回结果
  • 渲染视图

你看,它把整个流程串起来了。没有它,每个Controller都得自己处理这些杂事,那代码就乱套了。

2. HandlerMapping(处理器映射器)

DispatcherServlet拿到请求后,得知道该找谁干活。HandlerMapping就是干这个的。

它维护了一张“请求路径 → 处理器”的映射表。比如:

@RequestMapping("/user/list")
public String listUsers(Model model) {
    // 查询用户列表
    return "userList";
}

当请求 /user/list 进来时,HandlerMapping会找到 listUsers 这个方法。我见过不少新手在这里踩坑——路径写错了,或者忘了加 @RequestMapping,结果请求一直404。

我曾经在一个项目中,因为HandlerMapping配置了多个匹配规则,导致同一个请求被两个Controller同时处理。嗯,那天的线上事故,至今记忆犹新。

3. Controller(处理器/控制器)

这是真正干活的地方。Controller负责:

  • 解析请求参数
  • 调用业务逻辑(Service层)
  • 封装模型数据
  • 返回视图名称或数据

我个人习惯把Controller写得尽量薄。它只做“调度”,不做“实现”。你想想看,如果Controller里塞满了SQL和业务判断,那维护起来得多痛苦?

4. ModelAndView(模型与视图)

Controller处理完业务后,需要把结果返回给前端。ModelAndView就是装这个的容器。

它包含两部分:

  • Model:数据模型,比如用户列表、订单信息
  • View:视图名称,比如 userList.jsp

我记得早期做项目时,经常在Model里塞一堆不相关的数据。后来被同事吐槽:“你这Model都快成垃圾堆了。”从那以后,我养成了只放必要数据的习惯。

5. ViewResolver(视图解析器)

Controller返回的是逻辑视图名,比如 "userList"。ViewResolver负责把它解析成真正的视图文件,比如 /WEB-INF/views/userList.jsp

配置起来很简单:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/views/" />
    <property name="suffix" value=".jsp" />
</bean>

这样,你返回 "userList",它自动拼成 /WEB-INF/views/userList.jsp。省事吧?

组件之间的通信方式

搞清楚了组件,咱们再看看它们之间怎么通信的。说白了,就是一条“请求处理流水线”。

1. 请求 → DispatcherServlet

用户发来HTTP请求,Tomcat(或其他Servlet容器)把请求交给DispatcherServlet。

2. DispatcherServlet → HandlerMapping

DispatcherServlet问HandlerMapping:“这个请求该谁处理?”

3. HandlerMapping → Controller

HandlerMapping返回一个HandlerExecutionChain,里面包含了对应的Controller和方法。

4. Controller → Service

Controller调用Service层处理业务逻辑。这里要注意,Controller和Service之间是通过接口通信的。我习惯用 @Autowired 注入Service。

5. Controller → ModelAndView

Service返回结果后,Controller把数据封装到ModelAndView里。

6. DispatcherServlet → ViewResolver

DispatcherServlet拿到ModelAndView后,交给ViewResolver解析视图。

7. ViewResolver → 视图文件

ViewResolver找到JSP文件,渲染成HTML,返回给客户端。

你看,整个流程是单向的、线性的。这也是单体架构的特点——简单、直接、好理解。

核心要点:Spring MVC的通信方式本质上是“请求-响应”模式。每个请求都走一遍完整的流水线,组件之间通过接口或依赖注入耦合在一起。

一个完整的请求示例

光说理论太枯燥。咱们看个实际例子,你就明白了。

假设用户访问 /user/1,想查看ID为1的用户信息。

// Controller
@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public String getUser(@PathVariable Long id, Model model) {
        User user = userService.findById(id);
        model.addAttribute("user", user);
        return "userDetail";
    }
}

// Service
@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public User findById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

流程是这样的:

  1. 请求 /user/1 进来,DispatcherServlet接手
  2. HandlerMapping找到 UserController.getUser 方法
  3. Controller调用 userService.findById(1)
  4. Service从数据库查数据,返回User对象
  5. Controller把User放进Model,返回 "userDetail"
  6. ViewResolver解析成 /WEB-INF/views/userDetail.jsp
  7. JSP渲染HTML,返回给浏览器

嗯,就是这么简单。但简单背后也有隐患——所有组件都在一个进程里,一个地方出问题,整个应用就挂了。这也是我们后面要讲迁移的原因。

我的建议:在学习单体架构时,不要只关注“怎么用”,更要关注“为什么这么设计”。比如DispatcherServlet为什么是单例的?HandlerMapping为什么支持多种策略?想明白这些,你才能真正理解架构设计的精髓。

总结

这一章我们剖析了Spring MVC的核心组件和通信方式。说白了,它就是一套“请求处理流水线”:

  • DispatcherServlet:总调度
  • HandlerMapping:路由表
  • Controller:业务调度
  • ModelAndView:数据容器
  • ViewResolver:视图解析

组件之间通过接口和依赖注入通信,整个流程是线性的、同步的。这种架构简单易懂,适合中小型项目。但当你遇到高并发、复杂业务时,它的局限性就暴露出来了。

下一章,咱们聊聊单体架构的痛点——为什么我们要从它迁移到云原生?