本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:Java Spring MVC是一个基于Spring框架的轻量级Web开发框架,采用MVC架构实现业务逻辑与视图的解耦,支持依赖注入(DI)和面向切面编程(AOP)。本文介绍的“JavaSpringMvc的jar包”为Spring MVC的最小依赖集合,包含构建基础Web应用所需的核心JAR文件,适合初学者快速搭建项目。该集合涵盖spring-webmvc、spring-beans、spring-context等核心组件,并集成Servlet API、JSTL标签库及日志支持,帮助开发者完成从环境搭建到请求处理的全流程实践。通过手动导入这些JAR包,学习者可深入理解Spring MVC的依赖结构与运行机制,为进一步使用Maven/Gradle等构建工具打下坚实基础。

Spring MVC 核心架构与运行机制深度解析

在现代 Java Web 开发中,Spring MVC 依然是构建企业级应用的主流选择之一。它不仅仅是一个简单的 Web 框架,更是一套高度模块化、可扩展且与整个 Spring 生态无缝集成的设计体系。无论是微服务中的 REST API 层,还是传统多页面系统的后端渲染,Spring MVC 都以其清晰的职责分离和强大的配置灵活性支撑着复杂业务逻辑的稳定运行。

但你有没有遇到过这样的情况:项目启动时报错 ClassNotFoundException ,翻遍依赖却找不到根源?或者调试请求流程时,明明写了 @RequestMapping ,却被另一个处理器拦截了?又或者想自定义参数解析器,却发现不知道从哪切入?

这些问题的背后,往往不是代码写错了,而是对 Spring MVC 的底层运行机制缺乏“全景式”的理解。我们常常只关注如何用注解快速开发功能,却忽略了那些隐藏在 DispatcherServlet 背后的组件协作、JAR 包之间的依赖链条,以及 Bean 生命周期中的每一个钩子点。

今天,咱们就来一次“拆机式”剖析——不讲套路,不堆术语,而是像一位老工程师带着新人读源码那样,一层层揭开 Spring MVC 的神秘面纱。我们会从最基础的 JAR 包组成开始,看看每个 .jar 文件到底干了啥;然后深入到 DispatcherServlet 是如何调度请求的;再聊聊 Bean 容器是怎么管理对象生命周期的;最后还会带你玩转国际化、事件监听这些高级特性。

准备好了吗?🚀 让我们一起走进 Spring MVC 的心脏地带。


想象一下,你要搭建一个智能音箱的后台系统。这个系统需要处理语音指令、连接蓝牙设备、推送通知……听起来很复杂对吧?但实际上,它的 Web 控制层可能只需要几个接口: /voice/command /device/connect /notify/user 。这时候你会怎么做?直接上手写 Controller?等等!先别急着敲代码。

真正的高手,会先问自己三个问题:

  1. 我这个应用最少需要哪些 JAR 包才能跑起来?
  2. HTTP 请求进来之后,到底是谁在背后一步步把它送到我的方法里的?
  3. Controller 里的 userService 是谁给我的?它是单例吗?我能控制它的创建过程吗?

这三个问题,恰好对应了 Spring MVC 的三大核心支柱: 最小依赖集、MVC 执行链、IoC 容器管理 。接下来我们就一个一个来攻破。


最小 JAR 包集合:别让“自动导入”蒙蔽了双眼

现在大多数项目都用 Maven 或 Gradle 管理依赖,一行 <dependency> 就能引入一整套功能。这当然是好事,但也带来了一个副作用:很多人已经忘了“一个 Spring Web 应用究竟由哪些基本零件构成”。

不信你试试看:如果让你手动拷贝 .jar 文件到 WEB-INF/lib 目录下部署一个最简 Spring MVC 应用,你能列全所有必需的 JAR 吗?

别慌,我们一起来捋一捋。

四大金刚:spring-core、spring-beans、spring-context、spring-webmvc

Spring 框架采用的是典型的分层设计,就像搭积木一样,每一层都在前一层的基础上提供更高阶的能力。要跑通一个最基本的 Spring MVC 应用,你需要以下四个核心 JAR 包:

JAR 名称 功能定位 典型类
spring-core.jar 基础工具包,整个 Spring 的地基 ClassPathResource , StringUtils
spring-beans.jar Bean 的定义与生命周期管理 BeanFactory , BeanDefinition
spring-context.jar 应用上下文环境,支持事件、国际化等 ApplicationContext , MessageSource
spring-webmvc.jar Web 层封装,包含 DispatcherServlet DispatcherServlet , HandlerMapping

它们之间是层层依赖的关系,可以用一张图来表示:

graph TD
    A[spring-core.jar] --> B[spring-beans.jar]
    B --> C[spring-context.jar]
    C --> D[spring-webmvc.jar]
    style A fill:#4CAF50,stroke:#388E3C,color:white
    style B fill:#2196F3,stroke:#1976D2,color:white
    style C fill:#FF9800,stroke:#F57C00,color:white
    style D fill:#E91E63,stroke:#C2185B,color:white

    subgraph "Spring MVC 最小依赖栈"
        A
        B
        C
        D
    end

    click A "https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/package-summary.html" _blank
    click B "https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/package-summary.html" _blank
    click C "https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/package-summary.html" _blank
    click D "https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/package-summary.html" _blank

看到没? spring-webmvc 虽然是最上层的模块,但它其实是个“组合拳选手”——它自己并不实现 Bean 创建或资源加载,而是调用下层模块提供的能力。比如 DispatcherServlet 内部要用日志,那就会用到 spring-core 里的 Log 工具;要获取控制器实例,就得通过 spring-beans 提供的工厂机制。

所以如果你只引入了 spring-webmvc.jar 而漏掉了 spring-core.jar ,哪怕你的 Controller 写得再完美,服务器一启动就会抛出:

java.lang.NoClassDefFoundError: org/springframework/core/log/LogDelegateFactory

是不是有种“啊?我还以为一个 jar 就够了”的恍然大悟感?😅

实战演示:没有 XML 的极简上下文初始化

我们常说“Spring 是基于 IoC 容器的”,那你有没有亲手试过不用任何配置文件,纯代码启动一个 Spring 容器?

来,咱们动手写一段:

public class MinimalSpringDemo {
    public static void main(String[] args) {
        // 创建一个空的应用上下文
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();

        // 注册一个配置类(也可以注册普通 Bean)
        ctx.register(AppConfig.class);

        // 刷新容器,触发 Bean 加载
        ctx.refresh();

        // 获取 Bean 并使用
        MyService service = ctx.getBean(MyService.class);
        service.doSomething();

        // 关闭容器
        ctx.close();
    }
}

其中 AppConfig 是个简单的配置类:

@Configuration
@ComponentScan("com.example.service")
public class AppConfig {
}

这段代码虽然短,但它完整经历了 Spring 容器的核心生命周期:

  1. 创建上下文实例;
  2. 注册配置元数据;
  3. 解析并注册 BeanDefinition;
  4. 实例化 singleton Beans;
  5. 发布容器刷新事件;
  6. 进入就绪状态。

而这一切之所以能工作,正是因为 classpath 下有那四个核心 JAR 包各司其职。特别是 spring-context ,它不仅继承了 BeanFactory 的能力,还加入了自动扫描、事件发布、资源加载等企业级功能,可以说是“平民变贵族”的关键跃迁。

💡 小贴士 AnnotationConfigApplicationContext 属于 spring-context.jar ,而它内部使用的 DefaultListableBeanFactory 来自 spring-beans.jar 。这种跨 JAR 的协作非常普遍,也是为什么不能随意裁剪依赖的原因。


请求是如何被“路由”到你的 Controller 方法的?

现在假设我们的应用已经成功启动, DispatcherServlet 也已经在 web.xml 中注册好了。用户发送了一个 GET 请求 /user/123 ,希望查询 ID 为 123 的用户信息。

那么问题来了: 这个请求是怎么找到对应的 UserController.getUser() 方法的?中间经历了哪些步骤?

别急,让我们跟着 Spring MVC 的执行链走一遍。

第一步:前端控制器 DispatcherServlet 接收请求

所有进入系统的 HTTP 请求,只要匹配了你在 web.xml 中设置的 <url-pattern> (通常是 / ),都会被 Tomcat 交给 DispatcherServlet 处理。

<servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

DispatcherServlet 继承自 HttpServlet ,但它不像传统的 Servlet 那样直接写业务逻辑。相反,它扮演的是“总调度官”的角色——它不干活,只派活。

它的主要任务是协调以下几个组件完成请求处理:

  • HandlerMapping :找谁来处理?
  • HandlerAdapter :怎么调用那个处理器?
  • ViewResolver :返回的结果怎么渲染成页面?
  • HandlerExceptionResolver :出错了怎么办?

整个流程可以用一句话概括: 找人 → 调用 → 渲染 → 异常兜底

第二步:HandlerMapping 查找处理器

当请求到达时, DispatcherServlet 首先会遍历所有的 HandlerMapping 实现,询问:“谁能处理这个 /user/123 请求?”

最常见的 HandlerMapping RequestMappingHandlerMapping ,它会在应用启动时扫描所有带有 @Controller @RestController 注解的类,并解析其中的 @RequestMapping 注解,建立起 URL 到方法的映射表。

例如:

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

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
}

Spring 在启动时就会把这条规则记录下来:

GET /user/{id}  --> UserController.getUser(id)

等到真正收到请求时,只需查表即可快速定位目标方法。

多个 HandlerMapping 怎么办?优先级说了算!

你可能会问:系统里可以有多个 HandlerMapping 吗?当然可以!比如你还用了 SimpleUrlHandlerMapping 来手动配置一些静态路径映射。

这时候就需要排序了。Spring 使用 Ordered 接口来决定执行顺序,值越小优先级越高。

HandlerMapping 实现类 默认 Order 值 用途
RequestMappingHandlerMapping 0 处理注解式控制器 ✅ 主力军
BeanNameUrlHandlerMapping 2 按 Bean 名称匹配
SimpleUrlHandlerMapping 3 手动配置 URL 映射

建议显式设置 order,避免因版本升级导致行为变化。

第三步:HandlerAdapter 调用处理器方法

找到了处理器之后,下一步就是“调用它”。但注意,Spring MVC 支持多种类型的处理器:可以是带 @RequestMapping 的方法,也可以是实现了 HttpRequestHandler 接口的对象,甚至是老式的 Controller 接口实现类。

为了统一调用方式,Spring 引入了适配器模式 —— HandlerAdapter

最常见的适配器是 RequestMappingHandlerAdapter ,它不仅能调用方法,还能完成以下重要工作:

  • 参数解析(如 @PathVariable , @RequestParam
  • 数据绑定(将请求参数映射到 POJO)
  • 类型转换(String → Long, Date 等)
  • 校验(配合 @Valid

举个例子:

@GetMapping("/search")
public String searchUsers(
    @RequestParam(defaultValue = "1") int page,
    @RequestParam(required = false) String keyword,
    Model model) {

    Page<User> result = userService.search(keyword, page);
    model.addAttribute("users", result);
    return "user/list";
}

当你访问 /search?page=2&keyword=张 时, RequestMappingHandlerAdapter 会:

  1. 从请求中提取 page=2 ,自动转成 int
  2. 提取 keyword="张" ,赋值给形参;
  3. 创建一个 Model 对象并注入;
  4. 反射调用 searchUsers() 方法。

这一整套流程都是通过一系列 ArgumentResolver 实现的,比如:

  • RequestParamMethodArgumentResolver
  • PageableHandlerMethodArgumentResolver
  • ModelMethodProcessor

而且你可以扩展它!比如想支持 @CurrentUserId 注解自动注入当前登录用户的 ID:

public class CurrentUserIdArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(CurrentUserId.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, 
                                  ModelAndViewContainer container,
                                  NativeWebRequest request) {
        // 从 session 或 token 中解析用户 ID
        return SecurityUtils.getCurrentUserId();
    }
}

然后在配置中注册:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new CurrentUserIdArgumentResolver());
    }
}

瞧,这就是 Spring 的魅力所在:开放、灵活、可插拔。👏

第四步:返回值处理与视图渲染

方法执行完毕后,通常会有返回值。Spring 有一套完整的 HandlerMethodReturnValueHandler 链来处理不同类型的返回结果。

返回值类型 处理器 行为
String ViewNameMethodReturnValueHandler 当作逻辑视图名
ModelAndView ModelAndViewMethodReturnValueHandler 显式指定模型和视图
@ResponseBody RequestResponseBodyMethodProcessor 序列化为 JSON/XML
ResponseEntity HttpEntityMethodProcessor 支持自定义状态码和头

以最常见的 String 返回为例:

@RequestMapping("/home")
public String home(Model model) {
    model.addAttribute("title", "首页");
    return "homePage"; // ← 视图名
}

Spring 会将其交给 ViewResolver 去解析成实际的视图资源。

最常见的 ViewResolver InternalResourceViewResolver ,它可以自动拼接前缀和后缀:

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

这样,返回 "homePage" 就会被解析为 /WEB-INF/views/homePage.jsp ,并通过 RequestDispatcher.forward() 跳转。

当然,现在很多项目都不用 JSP 了,改用 Thymeleaf、Freemarker 甚至前后端分离。这时你可以配置多个 ViewResolver ,按优先级依次尝试:

<!-- JSP Resolver -->
<bean id="jspResolver" class="InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/jsp/" />
    <property name="suffix" value=".jsp" />
    <property name="order" value="1" />
</bean>

<!-- Thymeleaf Resolver -->
<bean id="thymeleafResolver" class="ThymeleafViewResolver">
    <property name="templateEngine" ref="templateEngine" />
    <property name="order" value="2" />
</bean>

如果第一个解析失败(比如文件不存在),就继续试下一个。这就实现了多模板共存。


双重宇宙:Root Context 与 Servlet Context 的层级关系

还记得我们在 web.xml 中既配置了 ContextLoaderListener ,又配置了 DispatcherServlet 吗?

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring-mvc-config.xml</param-value>
    </init-param>
</servlet>

这两者分别创建了两个不同的应用上下文:

  • Root WebApplicationContext :由 ContextLoaderListener 创建,用于存放 Service、Repository 等非 Web 层 Bean。
  • Servlet WebApplicationContext :由 DispatcherServlet 创建,专注于 Controller、HandlerMapping、ViewResolver 等 Web 层组件。

它们的关系如下图所示:

graph TD
    A[ServletContext] --> B[Root WebApplicationContext]
    A --> C[DispatcherServlet]
    C --> D[Servlet WebApplicationContext]
    B --> E[Service Beans]
    B --> F[Repository Beans]
    D --> G[Controller Beans]
    D --> H[HandlerMapping]
    D --> I[ViewResolver]

    style A fill:#f9f,stroke:#333
    style B fill:#bbf,stroke:#333,color:#fff
    style D fill:#bfb,stroke:#333,color:#fff

关键点在于: 子上下文可以访问父上下文中的 Bean,反之则不行

这意味着你可以在 Controller 中注入 Service:

@Controller
public class UserController {
    @Autowired
    private UserService userService; // ✅ 来自 Root Context
}

但在 Service 中不能注入 Controller:

@Service
public class UserService {
    @Autowired
    private UserController controller; // ❌ 编译不报错,运行时报 NoSuchBeanDefinitionException
}

这种设计的好处非常明显:

  • 解耦清晰 :Web 层和服务层分离,便于单元测试;
  • 资源共享 :多个 DispatcherServlet (如 /api/* /admin/* )可以共享同一套 Service;
  • 启动效率 :Root Context 只需初始化一次,提升性能。

所以在大型项目中,强烈建议使用双上下文结构,而不是把所有配置都塞进 *-servlet.xml 里。


Bean 的生命周期:不只是 new 一下那么简单

我们每天都在用 @Autowired ,但你有没有想过:Spring 是怎么创建这些 Bean 的?它们什么时候被实例化的?初始化方法又是怎么执行的?

来,我们画个完整的生命周期图谱:

graph TB
    A[实例化 Instantiation] --> B[属性填充 Populate Properties]
    B --> C[BeanNameAware.setBeanName]
    C --> D[BeanFactoryAware.setBeanFactory]
    D --> E[BeanPostProcessor.postProcessBeforeInitialization]
    E --> F[InitializingBean.afterPropertiesSet]
    F --> G[自定义 init-method]
    G --> H[BeanPostProcessor.postProcessAfterInitialization]
    H --> I[Bean ready for use]
    I --> J[DisposableBean.destroy]
    J --> K[自定义 destroy-method]

每一步都有对应的回调接口或注解支持:

阶段 实现方式
实例化 构造函数、工厂方法
属性注入 @Autowired , @Value , XML 配置
初始化前 BeanPostProcessor.postProcessBeforeInitialization
初始化 @PostConstruct , InitializingBean , init-method
初始化后 BeanPostProcessor.postProcessAfterInitialization
销毁 @PreDestroy , DisposableBean , destroy-method

特别值得一提的是 BeanPostProcessor ,它是 Spring 最强大的扩展点之一。AOP 代理、@Async、@Scheduled 等功能都是通过它实现的。

比如你想在某个特定类型的 Bean 创建完成后打个日志:

@Component
public class LoggingBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        if (bean instanceof UserService) {
            System.out.println("✅ UserService [" + beanName + "] 已初始化完成!");
        }
        return bean;
    }
}

是不是感觉突然掌握了“上帝视角”?😎


更多杀手级特性:你可能还没用过的 Spring 黑科技

到现在为止,我们已经讲完了 Spring MVC 的主干内容。但 Spring 的精彩远不止于此。下面这几个特性,一旦掌握就能让你的代码质量上升好几个档次。

国际化:轻松支持多语言

很多系统都需要支持中文、英文、日文等多种语言。Spring 提供了 MessageSource 接口来统一管理资源文件。

首先准备两个 properties 文件:

# messages_zh.properties
welcome.message=欢迎访问系统
button.login=登录

# messages_en.properties
welcome.message=Welcome to the system
button.login=Login

然后在 Spring 配置中注册:

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basename" value="messages" />
    <property name="defaultEncoding" value="UTF-8" />
</bean>

控制器中就可以动态获取翻译文本:

@GetMapping("/")
public String home(Model model, Locale locale) {
    String msg = messageSource.getMessage("welcome.message", null, locale);
    model.addAttribute("msg", msg);
    return "index";
}

JSP 页面用标签库:

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<h1><spring:message code="welcome.message"/></h1>

简单吧?再也不用手动维护一堆 if-else 判断语言了。

事件驱动:优雅地解耦业务逻辑

当用户注册成功后,你可能需要做几件事:发送欢迎邮件、记录操作日志、更新推荐人积分……如果全都写在 register() 方法里,代码会越来越臃肿。

更好的做法是发布一个事件,让其他模块去监听:

// 定义事件
public class UserRegisteredEvent extends ApplicationEvent {
    private final String email;

    public UserRegisteredEvent(Object source, String email) {
        super(source);
        this.email = email;
    }

    public String getEmail() { return email; }
}

// 发布事件
@Service
public class UserService {
    @Autowired
    private ApplicationEventPublisher publisher;

    public void register(String email) {
        // 保存用户...
        publisher.publishEvent(new UserRegisteredEvent(this, email));
    }
}

// 监听事件
@Component
public class EmailNotificationListener {
    @EventListener
    public void sendWelcomeEmail(UserRegisteredEvent event) {
        System.out.println("📧 发送欢迎邮件给:" + event.getEmail());
    }
}

这种方式完全解耦,新增功能只需加个监听器,不影响原有逻辑。

SpEL 表达式:运行时动态计算值

Spring Expression Language(SpEL)是一种强大的表达式语言,可以在配置中动态计算值。

比如你想根据环境变量决定是否启用缓存:

@Value("#{systemEnvironment['ENV'] == 'prod'}")
private boolean enableCache;

或者注入一个随机数:

@Value("#{T(java.lang.Math).random() * 100}")
private double randomValue;

甚至可以用 Elvis 操作符设置默认值:

@Value("#{environment['HOME_DIR'] ?: '/tmp'}")
private String homeDir;

SpEL 的能力远超你的想象,特别是在条件装配、动态配置中非常实用。


总结:成为真正的 Spring 掌门人

看完这一整套流程,你现在应该明白:

  • Spring MVC 不是魔法 ,它的每一个功能背后都有清晰的组件协作;
  • 依赖不能乱删 ,每一个 JAR 包都有明确的职责边界;
  • 请求处理是有迹可循的 ,从 DispatcherServlet 到最终方法调用,每一步都可以干预;
  • Bean 的生命周期是可以掌控的 ,你可以精确控制对象的创建、初始化和销毁;
  • Spring 提供了大量扩展点 ,只要你愿意深入,几乎没有做不到的事。

所以,下次当你遇到问题时,不要再盲目搜索“怎么解决 NoClassDefFoundError”或者“为什么 @Autowired 不生效”了。试着回到源头,问问自己:

“这是哪个组件负责的?”
“它依赖什么?”
“我在哪个阶段介入最合适?”

这才是真正高级程序员的思维方式。🧠💡

Spring 就像一辆精密的跑车,只有了解它的发动机、变速箱和悬挂系统,你才能把它开到极限。而现在,你已经拿到了这辆车的全套维修手册。

祝你在 Spring 的世界里,驰骋自如,游刃有余!🚗💨

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:Java Spring MVC是一个基于Spring框架的轻量级Web开发框架,采用MVC架构实现业务逻辑与视图的解耦,支持依赖注入(DI)和面向切面编程(AOP)。本文介绍的“JavaSpringMvc的jar包”为Spring MVC的最小依赖集合,包含构建基础Web应用所需的核心JAR文件,适合初学者快速搭建项目。该集合涵盖spring-webmvc、spring-beans、spring-context等核心组件,并集成Servlet API、JSTL标签库及日志支持,帮助开发者完成从环境搭建到请求处理的全流程实践。通过手动导入这些JAR包,学习者可深入理解Spring MVC的依赖结构与运行机制,为进一步使用Maven/Gradle等构建工具打下坚实基础。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐