手动整合Spring与SpringMVC:深入理解框架底层原理的实战指南
1. 为什么还要死磕 Spring SpringMVC 这套“老组合”先把结论摆在前面Spring SpringMVC 这套组合放到今天依然是最适合用来打通“框架底层认知”的练手项目。你可能已经在用 Spring Boot 写业务了注解一贴、启动类一跑接口就通了爽是爽但很多人其实说不清楚一个请求从浏览器发出到返回 JSON中间到底经过了哪些环节容器里的 Bean 是怎么被创建、装配、代理的事务又是在哪一层悄悄生效的。这些疑问只有把 Spring 和 SpringMVC 拆开、手动整合一遍才会真正落地。这个综合练习的核心目标就是脱离 Spring Boot 的自动装配用最原始的方式把 Spring 容器和 SpringMVC 前端控制器拼到一起跑通一个完整的增删改查接口。它解决的问题不是“怎么快速上线”而是“怎么真正理解这套框架的运转机制”。适合谁参考适合已经会写 Spring Boot 业务、但想回头补底层原理的开发者适合准备面试、被问到“SpringMVC 请求流程”“IOC 容器启动过程”时心里发虚的同学也适合想自己动手写个小框架、需要先摸清标准实现的人。我个人的判断是自动装配是“结果”手动整合才是“过程”。只吃结果遇到诡异问题就只能靠猜走过一遍过程很多报错你一眼就能定位。下面我就按实际动手的顺序把整个练习的设计思路、关键细节、完整流程和踩坑记录摊开讲。2. 整体设计与思路拆解2.1 为什么不用 Spring Boot偏要手动整合很多人第一反应是都什么年代了还手动配 web.xml这不是倒退吗。恰恰相反手动整合的价值在于“可控”。Spring Boot 帮你做了几百个自动配置类你写错一个依赖它可能默默帮你兜底也可能默默帮你埋雷。手动整合时每一个 Bean、每一个配置都得你自己声明出问题一定是你的配置有问题排查路径极短。从架构上看这个练习要搭的是一个典型的父子容器结构。Spring 的根容器Root ApplicationContext负责管理 Service、DAO、数据源、事务管理器这些“业务层组件”SpringMVC 的子容器WebApplicationContext负责管理 Controller、视图解析器、消息转换器这些“Web 层组件”。子容器可以访问父容器的 Bean父容器访问不到子容器的 Bean。这个设计不是随便定的它决定了你的事务能不能生效、Controller 能不能注入 Service。注意父子容器是这套练习里最容易翻车的点。如果你把 Service 扫进了 SpringMVC 子容器事务大概率失效因为事务管理器在父容器里子容器里的 Service 拿不到它。2.2 分层结构与职责边界整个项目我按经典的四层来切Controller 层、Service 层、DAO 层、实体层。Controller 只做参数接收和结果返回不写业务逻辑Service 负责业务编排和事务边界DAO 只跟数据库打交道实体层就是数据载体。这个分层看起来老套但它是理解 Spring 依赖注入最直观的载体——每一层的 Bean 由谁创建、注入给谁一目了然。选型上持久层我用最朴素的 JdbcTemplate而不是 MyBatis 或 JPA。原因很简单这个练习的重点是 Spring 的容器和 AOP不是 ORM。JdbcTemplate 足够轻配置少能让我把注意力集中在容器装配上。如果你用 MyBatis也没问题但要多配一个 SqlSessionFactory反而分散了主线。2.3 依赖版本的选择逻辑版本这块我踩过坑必须说清楚。Spring 5.x 和 Spring 6.x 差别很大Spring 6 要求 JDK 17 起步而且把很多 javax 包换成了 jakarta 包。如果你用的是 JDK 8 或 11老老实实选 Spring 5.3.x这是 5.x 最后一个稳定分支社区资料最全。Servlet API 对应的是 javax.servlet别手滑引成 jakarta.servlet否则启动直接报 ClassNotFound。组件推荐版本说明JDK8 / 11与 Spring 5.3 兼容性最好Spring5.3.x5.x 最终稳定分支Servlet API4.0.1对应 javax.servlet 命名空间数据库MySQL 8.x驱动用 mysql-connector-java 8.x连接池Druid 或 HikariCP二选一别同时引日志SLF4J Logback排查容器启动问题全靠它3. 核心细节解析与实操要点3.1 父子容器的配置要点根容器的配置我放在applicationContext.xml里只扫 Service 和 DAO 包同时配置数据源、事务管理器、事务注解驱动。子容器的配置放在spring-mvc.xml里只扫 Controller 包配置注解驱动、视图解析器、静态资源处理。两个配置文件各管一摊边界清晰。!-- applicationContext.xml 根容器 -- context:component-scan base-packagecom.demo.service,com.demo.dao/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property nameurl valuejdbc:mysql://localhost:3306/demo/ property nameusername valueroot/ property namepassword value123456/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/!-- spring-mvc.xml 子容器 -- context:component-scan base-packagecom.demo.controller/ mvc:annotation-driven/ mvc:default-servlet-handler/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean这里有个细节mvc:default-servlet-handler/一定要加否则静态资源css、js、图片会被 DispatcherServlet 拦截全部 404。这个坑我当年找了半天最后发现是 DispatcherServlet 默认映射/把所有请求都吃了。3.2 web.xml 的加载顺序web.xml 是整个应用的入口加载顺序错了容器就起不来。正确的顺序是先配 ContextLoaderListener 加载根容器再配 DispatcherServlet 加载子容器。ContextLoaderListener 默认读applicationContext.xmlDispatcherServlet 默认读[servlet-name]-servlet.xml我习惯显式指定避免歧义。listener listener-class org.springframework.web.context.ContextLoaderListener /listener-class /listener context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param servlet servlet-namedispatcher/servlet-name servlet-class org.springframework.web.servlet.DispatcherServlet /servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping提示load-on-startup设为 1让 DispatcherServlet 随容器启动就初始化而不是等第一个请求来了才初始化。这样启动阶段就能暴露配置错误别等到线上才发现。3.3 事务边界的确定事务加在哪一层是个原则问题。我统一加在 Service 层的方法上用Transactional。Controller 不加事务DAO 也不加。原因在于事务的粒度应该对应一个完整的业务操作而业务操作是在 Service 层编排的。如果加在 DAO一个业务调多个 DAO 方法就没法保证原子性如果加在 Controller事务范围又太大把参数校验、结果封装都包进去了白白占用连接。Service public class UserServiceImpl implements UserService { Autowired private UserDao userDao; Override Transactional(rollbackFor Exception.class) public void transfer(int fromId, int toId, BigDecimal amount) { userDao.deduct(fromId, amount); userDao.add(toId, amount); } }注意rollbackFor Exception.class。Spring 默认只对 RuntimeException 和 Error 回滚受检异常不回滚。这个默认行为坑过无数人业务里抛了自定义的受检异常事务没回滚数据就脏了。养成显式声明的习惯。4. 实操过程与核心环节实现4.1 环境搭建与依赖引入Maven 的 pom 里Spring 的核心依赖是 spring-context、spring-webmvc、spring-jdbc、spring-tx再加 servlet-apiscope 设 provided、数据库驱动、连接池、日志。这里有个常见错误把 spring-web 和 spring-webmvc 搞混。spring-web 提供的是基础 Web 支持spring-webmvc 才是 MVC 框架本体两个都要引但 webmvc 会传递依赖 web所以引 webmvc 就够了。dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.30/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version5.3.30/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency4.2 一个完整请求的流转记录我拿“查询用户详情”这个接口来走一遍全流程把每个环节都标出来。请求是GET /user/detail?id1。第一步请求到达 Tomcat根据 url-pattern/匹配到 DispatcherServlet。第二步DispatcherServlet 从自己的子容器里拿到 HandlerMapping由mvc:annotation-driven/注册根据路径找到对应的 Controller 方法。第三步HandlerAdapter 负责调用这个方法期间完成参数绑定——把id1转成 int 注入方法参数。第四步方法内部调用 ServiceService 调用 DAODAO 用 JdbcTemplate 查库。第五步返回的 User 对象被消息转换器如果是 ResponseBody序列化成 JSON或者被视图解析器解析成 JSP 路径。第六步响应写回浏览器。这个流程里最值得琢磨的是第三步的参数绑定。SpringMVC 的RequestParam、PathVariable、RequestBody分别对应不同的解析器理解它们的区别才能写出正确的接口。Controller RequestMapping(/user) public class UserController { Autowired private UserService userService; RequestMapping(/detail) ResponseBody public User detail(RequestParam(id) int id) { return userService.getById(id); } }4.3 参数绑定与返回值处理参数绑定这块我总结了几个高频场景。简单类型用RequestParam路径变量用PathVariableJSON 体用RequestBody表单对象直接写实体类参数即可自动封装。返回值方面返回 String 默认当视图名加ResponseBody才当数据返回对象加ResponseBody会被 Jackson 序列化。场景注解示例查询参数RequestParam/detail?id1路径变量PathVariable/detail/1JSON 请求体RequestBodyPOST JSON表单对象无注解字段名匹配自动封装返回 JSONResponseBody对象自动序列化有个细节要注意RequestParam默认 requiredtrue参数不传直接 400。如果参数可选得写required false并给默认值否则前端漏传就报错。4.4 数据源与 JdbcTemplate 的装配数据源我用 Druid配置了初始连接数、最大连接数、超时时间。JdbcTemplate 直接注入 dataSource 即可Spring 会自动帮它管理连接的获取和释放。这里的关键是JdbcTemplate 是线程安全的可以做成单例 Bean全局共享一个实例。Repository public class UserDaoImpl implements UserDao { Autowired private JdbcTemplate jdbcTemplate; Override public User getById(int id) { String sql select id, name, balance from t_user where id ?; return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper(User.class), id); } }BeanPropertyRowMapper会自动把结果集的列名映射到实体属性列名user_name对应属性userName下划线转驼峰它帮你做了。这个类省了大量手写 RowMapper 的功夫但要注意列名和属性名必须能对上对不上就映射成 null。5. 常见问题与排查技巧实录5.1 容器启动失败的排查顺序容器起不来日志是第一手线索。我的排查顺序是先看有没有 ClassNotFound通常是依赖没引全或版本冲突再看有没有 NoSuchBeanDefinition通常是包扫描路径写错或注解没加最后看有没有循环依赖通常是构造器注入互相引用。这三类占了启动失败的九成。报错关键词大概率原因解决方向ClassNotFoundException依赖缺失/版本冲突mvn dependency:tree 排查NoSuchBeanDefinition扫描路径错/注解缺失检查 component-scanBeanCurrentlyInCreation循环依赖改 setter 注入或 LazyNoClassDefFoundError运行时缺包检查 scope 是否 provided5.2 事务不生效的三种典型情况事务不生效是这套练习里最经典的坑。第一种Service 没被 Spring 管理自己 new 出来的对象事务注解形同虚设。第二种方法不是 publicSpring 的 AOP 代理对非 public 方法不生效。第三种同类内部方法调用this.method()走的是原始对象绕过了代理。这三种我都踩过尤其是第三种隐蔽性极强。注意同类内部调用想走事务要么把方法拆到另一个 Bean要么注入自己Autowired private UserService self要么用 AopContext.currentProxy()。最推荐第一种结构最清晰。5.3 中文乱码与静态资源 404中文乱码分两种请求乱码和响应乱码。请求乱码用 CharacterEncodingFilter 解决注意要配在 DispatcherServlet 之前且 forceEncoding 设为 true。响应乱码在RequestMapping上配 produces application/json;charsetUTF-8。静态资源 404 前面提过加mvc:default-servlet-handler/即可但要注意它和mvc:annotation-driven/的配合两个都要有。filter filter-nameencodingFilter/filter-name filter-class org.springframework.web.filter.CharacterEncodingFilter /filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping5.4 独家避坑心得分享几个文档里不会写、但实际很要命的经验。第一父子容器的扫描包千万别重叠一旦某个 Bean 被两个容器都扫到会出现两个实例事务和注入全乱套。第二mvc:annotation-driven/和手动配 RequestMappingHandlerMapping 二选一同时配会冲突。第三JdbcTemplate 的 queryForObject 查不到数据会抛 EmptyResultDataAccessException不是返回 null业务里要 try-catch 或改用 query 方法。第四Druid 的监控页面记得关掉或加权限别裸奔在公网。这套练习做完我对 Spring 的理解从“会用”变成了“知道为什么”。后面再去看 Spring Boot 的自动配置源码那些 ConditionalOnMissingBean 的判断逻辑一下子就看懂了——因为我知道它到底在替我配什么。如果你也想补这一课建议别偷懒用 Boot老老实实手动整合一遍收获完全不一样。

相关新闻

Java+SpringBoot+SSM爱心公益网站系统开发实战:从需求分解到部署上线

Java+SpringBoot+SSM爱心公益网站系统开发实战:从需求分解到部署上线

接手过这么多公益类网站的需求,"基于JavaSpringBootSSM爱心公益网站"这个标题我可以说非常熟悉。很多同学拿到这种课设或毕设题目时,第一反应是上网找个模板改改,结果项目做完也说不清自己干了什么。这篇文章我把自己做类似项目时的…

2026/10/15 13:42:12 阅读更多 →
阿培利司Vijoice治疗PROS:PIK3CA体细胞突变的靶向治疗逻辑与适应症

阿培利司Vijoice治疗PROS:PIK3CA体细胞突变的靶向治疗逻辑与适应症

PIK3CA 相关过度生长谱系疾病(PROS)是一类由 PIK3CA 体细胞激活突变驱动的罕见病谱系,包含 CLOVES 综合征、巨脑 - 毛细血管畸形综合征(MCAP/M-CM)、Klippel‑Trenaunay 综合征、面部浸润性脂肪瘤病等多种表型&#xf…

2026/10/15 13:42:12 阅读更多 →
【方达炬治学】方达炬:一个国家的领导权,衰弱,崛起;看它的年薪,货,时机;

【方达炬治学】方达炬:一个国家的领导权,衰弱,崛起;看它的年薪,货,时机;

【方达炬治学】方达炬:一个国家的领导权,衰弱,崛起;看它的年薪,货,时机;

2026/10/15 13:41:11 阅读更多 →

最新新闻

克林纱是什么?格林丝曲丝涤纶装饰纱选型安装指南

克林纱是什么?格林丝曲丝涤纶装饰纱选型安装指南

提到克林纱,很多第一次跑面料市场的人都会愣一下——同一个卷轴上明明写着“格林丝”,隔壁摊位又管它叫“曲丝”,翻翻标签还看到“克林纱”三个字。其实就是同一种东西:一种以涤纶为主要原料,织造时让纱线保持轻微卷曲…

2026/10/15 14:20:23 阅读更多 →
从 REA 到 libuvcpp:当 AI 能“读懂”软件,框架的未来形态是什么?

从 REA 到 libuvcpp:当 AI 能“读懂”软件,框架的未来形态是什么?

从 REA 到 libuvcpp:当 AI 能“读懂”软件,框架的未来形态是什么? 2026 年 10 月,一个叫 REA 的项目三天内拿下了 12 万颗 GitHub 星。它让 AI 能够逆向分析任何软件,理解功能实现,甚至尝试写出相似功能的代…

2026/10/15 14:20:23 阅读更多 →
30-seconds-of-python 切片技巧:用 take() 截取列表前 N 个元素

30-seconds-of-python 切片技巧:用 take() 截取列表前 N 个元素

教程文档 【免费下载链接】30-seconds-of-python Short Python code snippets for all your development needs 项目地址: https://gitcode.com/gh_mirrors/30/30-seconds-of-python 点击查看 免费下载 本指南围绕 30-seconds-of-python 仓库中的 take 代码片段 展…

2026/10/15 14:20:23 阅读更多 →
C++调用命令行程序的三种方法:从Shell重定向到Boost.Process实战

C++调用命令行程序的三种方法:从Shell重定向到Boost.Process实战

1. 项目概述:为什么命令行输出重定向不是“写个 > 就完事”的小事在 Windows 开发、自动化脚本、CI/CD 流水线或本地工具链集成中,我几乎每天都要和cmd.exe打交道——不是为了怀旧,而是因为很多老旧但关键的内部工具、硬件驱动配套程序、第…

2026/10/15 14:20:23 阅读更多 →
一个插件管理多种音乐源:Tubifarry Web客户端(Lucida / DABMusic / T2Tunes / Subsonic)完整配置清单

一个插件管理多种音乐源:Tubifarry Web客户端(Lucida / DABMusic / T2Tunes / Subsonic)完整配置清单

【免费下载链接】Tubifarry Tubifarry is a Lidarr plugin that enhances your music library by fetching music from YouTube, integrating with Slskd for Soulseek access, automating Spotify playlist imports, converting files, and retrieving soundtracks from Radar…

2026/10/15 14:20:23 阅读更多 →
海外仓运作全攻略:从选型、头程到仓内作业与避坑指南

海外仓运作全攻略:从选型、头程到仓内作业与避坑指南

简介:海外仓运作内部培训PPT,定位跨境电商物流从业者、海外仓运营及物流产品相关岗位,旨在帮助读者系统理解海外仓从货物备货、头程运输、入库上架、库内管理,到尾程派送、售后服务及增值服务的全链路运营逻辑。内容以头程、尾程、…

2026/10/15 14:19:22 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →