资讯动态

Spring+SpringMVC综合练习:父子容器与事务管理实战

发布时间:2026/10/15 15:40:31 来源:尧图企业网站定制
1. 项目整体设计与思路拆解1.1 为什么选择SpringSpringMVC做综合练习很多刚学完Spring和SpringMVC的开发者单独看IoC、AOP、DispatcherServlet这些概念都能理解但一旦要把它们串起来做一个完整的东西就容易卡壳。这个综合练习的核心目的就是解决“知识点都会、但拼不起来”的问题。我当初做这个练习的时候最大的感受是Spring和SpringMVC的关系本质上就是“父容器管Service和DAO子容器管Controller”的关系。这个父子容器的概念如果你只背面试题可能觉得就是一句话但真正在项目里踩过坑之后你才会深刻理解为什么要有这层设计。选择SpringSpringMVC而不是直接上Spring Boot理由也很简单Spring Boot帮你自动配置了太多东西你根本看不到DispatcherServlet是怎么注册的、HandlerMapping是怎么工作的、ViewResolver是怎么解析视图的。做综合练习就是要用最“原始”的方式把每个环节都手动配一遍这样才能真正理解框架的运作机制。1.2 这个练习适合谁来动手这个综合练习适合三类人刚学完Spring基础、想验证自己是否真懂的人如果你能独立把这个练习从头搭起来说明你对IoC和DI的理解已经过关了。准备面试、需要项目经验支撑的人面试官问“SpringMVC的请求流程”你能结合自己搭过的项目讲比背八股文强十倍。从Servlet时代过渡到框架开发的人如果你之前写原生Servlet这个练习能帮你理解框架到底帮你省了什么。不适合的人群也说一下如果你连Maven依赖管理都不熟、Tomcat都没部署过建议先把这些基础补上再来否则你会把大量时间花在环境问题上而不是框架本身。1.3 整体架构设计思路这个练习我采用的架构是经典的三层结构Controller层 → Service层 → DAO层。看起来简单但每一层的职责划分和容器归属是有讲究的。Controller层由SpringMVC子容器管理负责接收请求、参数绑定、调用Service、返回视图。Service层和DAO层由Spring父容器管理负责业务逻辑和数据访问。为什么这么分因为Controller需要用到Service子容器可以访问父容器的Bean但父容器访问不到子容器的Bean。这个单向可见性保证了Service层不会反向依赖Controller层架构上更干净。数据访问层我用的是JdbcTemplate而不是MyBatis或Hibernate。原因很直接综合练习的重点是Spring的整合能力不是ORM框架的使用。JdbcTemplate足够简单能让我把精力集中在Spring的配置和容器管理上。如果你已经熟悉MyBatis完全可以替换掉核心思路不变。2. 核心细节解析与实操要点2.1 父子容器的配置要点这是整个练习最容易出错的地方我单独拎出来讲。在web.xml中你需要配置两个东西ContextLoaderListener和DispatcherServlet。ContextLoaderListener加载的是父容器通常叫applicationContext.xml里面扫描Service和DAO。DispatcherServlet加载的是子容器通常叫spring-mvc.xml里面扫描Controller。!-- web.xml 核心配置 -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener servlet servlet-namedispatcher/servlet-name servlet-classorg.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注意url-pattern//url-pattern和url-pattern/*/url-pattern的区别非常大。用/会覆盖Tomcat的默认Servlet静态资源需要额外配置放行用/*会拦截所有请求包括JSP导致视图解析出问题。这个坑我踩过排查了大半天。2.2 包扫描的隔离策略父容器和子容器的包扫描必须严格隔离否则会出现Bean被重复创建的问题。我的做法是父容器applicationContext.xmlcontext:component-scan base-packagecom.example.service,com.example.dao/子容器spring-mvc.xmlcontext:component-scan base-packagecom.example.controller/如果你图省事两边都写base-packagecom.example那Controller会被创建两次——一次在父容器、一次在子容器。子容器中的Controller能正常工作但父容器中那个多余的Controller会浪费资源更严重的是可能导致事务配置不生效因为事务管理器在父容器而父容器里的Controller引用的Service可能是代理前的原始对象。2.3 事务管理的配置细节事务管理器必须配置在父容器中因为它管理的是Service层的方法。配置方式有两种注解驱动和XML配置。我推荐用注解方式更简洁!-- applicationContext.xml 中 -- bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/然后在Service类或方法上加Transactional注解即可。这里有个细节tx:annotation-driven默认只对同一个容器中的Bean生效。因为Service在父容器事务管理器也在父容器所以没问题。但如果你不小心把Service扫描到了子容器事务就不会生效——这是父子容器隔离没做好最常见的后果之一。3. 实操过程与核心环节实现3.1 项目骨架搭建与依赖配置先建一个Maven Web项目pom.xml中引入核心依赖。版本选择上Spring 5.x配合JDK 8或11都比较稳Servlet API用3.1以上。properties spring.version5.3.30/spring.version /properties dependencies !-- Spring核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency !-- 数据源 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency !-- 数据库驱动按实际使用的数据库选择 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency !-- Servlet API -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSTL -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies提示spring-webmvc会自动引入spring-web和spring-core等基础模块不需要重复声明。但spring-jdbc需要单独引入它不在webmvc的传递依赖里。3.2 数据源与JdbcTemplate配置数据源配置我放在applicationContext.xml中用Druid连接池。这里把数据库连接参数单独放到jdbc.properties文件里方便环境切换。# jdbc.properties jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/practice_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.usernameroot jdbc.passwordyour_password!-- applicationContext.xml -- context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ property nameinitialSize value5/ property namemaxActive value20/ property nameminIdle value5/ property namemaxWait value60000/ /bean bean idjdbcTemplate classorg.springframework.jdbc.core.JdbcTemplate property namedataSource refdataSource/ /bean连接池参数的计算逻辑initialSize设为5是因为开发环境并发不高启动时建立5个连接足够maxActive设为20是考虑到测试时可能有并发请求但又不至于把数据库连接数占满maxWait设为60000毫秒60秒意思是当连接池耗尽时请求最多等60秒超过就抛异常避免请求无限期挂起。3.3 DAO层实现以用户表的操作为例写一个UserDao。这里用JdbcTemplate的query方法和RowMapper来做结果集映射。Repository public class UserDao { Autowired private JdbcTemplate jdbcTemplate; private static final RowMapperUser USER_ROW_MAPPER (rs, rowNum) - { User user new User(); user.setId(rs.getLong(id)); user.setUsername(rs.getString(username)); user.setEmail(rs.getString(email)); user.setCreateTime(rs.getTimestamp(create_time)); return user; }; public User findById(Long id) { String sql SELECT id, username, email, create_time FROM t_user WHERE id ?; ListUser list jdbcTemplate.query(sql, USER_ROW_MAPPER, id); return list.isEmpty() ? null : list.get(0); } public ListUser findAll() { String sql SELECT id, username, email, create_time FROM t_user ORDER BY id; return jdbcTemplate.query(sql, USER_ROW_MAPPER); } public int insert(User user) { String sql INSERT INTO t_user(username, email, create_time) VALUES(?, ?, ?); return jdbcTemplate.update(sql, user.getUsername(), user.getEmail(), new Timestamp(System.currentTimeMillis())); } public int update(User user) { String sql UPDATE t_user SET username ?, email ? WHERE id ?; return jdbcTemplate.update(sql, user.getUsername(), user.getEmail(), user.getId()); } public int deleteById(Long id) { String sql DELETE FROM t_user WHERE id ?; return jdbcTemplate.update(sql, id); } }注意RowMapper我定义成了静态常量而不是每次查询都new一个。因为RowMapper是无状态的复用同一个实例可以减少对象创建开销。这个细节在QPS高的场景下有意义练习中养成习惯也好。3.4 Service层实现与事务控制Service层注入DAO并在需要事务的方法上加Transactional。这里以转账场景为例演示事务的必要性。Service public class UserService { Autowired private UserDao userDao; Autowired private AccountDao accountDao; public User getUserById(Long id) { return userDao.findById(id); } public ListUser listAllUsers() { return userDao.findAll(); } Transactional(rollbackFor Exception.class) public void transfer(Long fromId, Long toId, BigDecimal amount) { // 扣款 accountDao.decreaseBalance(fromId, amount); // 模拟异常验证事务回滚 if (amount.compareTo(new BigDecimal(10000)) 0) { throw new RuntimeException(单笔转账金额超限); } // 入账 accountDao.increaseBalance(toId, amount); } }rollbackFor Exception.class这个配置很关键。Spring默认只对RuntimeException和Error回滚对受检异常Exception的子类但不是RuntimeException不回滚。加上这个配置后任何异常都会触发回滚避免出现“扣了钱没到账”的事故。3.5 Controller层实现与请求映射Controller层用Controller和RequestMapping来处理请求。我习惯在类级别加一个基础路径方法级别再加具体路径这样URL结构清晰。Controller RequestMapping(/user) public class UserController { Autowired private UserService userService; RequestMapping(/list) public String list(Model model) { ListUser users userService.listAllUsers(); model.addAttribute(users, users); return user/list; } RequestMapping(value /add, method RequestMethod.POST) public String add(User user, RedirectAttributes redirectAttributes) { userService.addUser(user); redirectAttributes.addFlashAttribute(msg, 添加成功); return redirect:/user/list; } RequestMapping(/detail/{id}) public String detail(PathVariable(id) Long id, Model model) { User user userService.getUserById(id); model.addAttribute(user, user); return user/detail; } }RedirectAttributes的addFlashAttribute方法用于重定向后传递一次性数据底层是放到Session中重定向完成后自动清除。这个在“添加成功后跳转到列表页并显示提示”的场景中非常实用比手动拼URL参数优雅得多。3.6 视图解析器配置spring-mvc.xml中配置视图解析器把逻辑视图名映射到实际的JSP文件。bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean !-- 静态资源放行 -- mvc:default-servlet-handler/ mvc:annotation-driven/prefix设为/WEB-INF/views/是为了安全——WEB-INF目录下的JSP无法被浏览器直接访问必须经过Controller转发这样能防止用户绕过Controller直接访问视图。mvc:default-servlet-handler/是配合url-pattern//url-pattern使用的让静态资源CSS、JS、图片交给Tomcat的默认Servlet处理。4. 常见问题与排查技巧实录4.1 启动报错与配置问题速查下面这张表是我在多次搭建过程中整理出来的高频问题基本覆盖了80%以上的启动失败场景。报错信息根本原因解决方案NoSuchBeanDefinitionException包扫描路径不对或Bean未加注解检查component-scan的base-package是否覆盖到目标类NoClassDefFoundError: javax/servlet/DispatcherServletservlet-api依赖scope错误确保scope为provided且Tomcat版本匹配事务不生效异常后数据未回滚Service被扫描到子容器或未加Transactional确认Service在父容器扫描路径下检查注解位置静态资源404DispatcherServlet拦截了所有请求添加mvc:default-servlet-handler/中文乱码请求或响应编码未设置配置CharacterEncodingFilterJSP页面设pageEncodingUTF-8JdbcTemplate注入失败未配置JdbcTemplate Bean或数据源检查applicationContext.xml中是否声明了jdbcTemplate4.2 父子容器导致事务失效的排查过程这个问题值得单独讲因为它太隐蔽了。现象是Service方法抛了异常但数据库数据没有回滚。我一开始以为是Transactional没加检查后发现加了又以为是事务管理器配置有问题检查后也没问题。最后通过打印Service对象的类名才发现注入到Controller里的Service是一个原始对象不是代理对象。原因就是Controller在子容器Service在父容器。子容器扫描时如果base-package写得太宽把Service也扫进了子容器那么子容器中会创建一个Service实例没有事务代理而Controller注入的正是这个子容器的实例。父容器中那个带事务代理的Service反而没人用。解决办法就是严格隔离包扫描路径Controller只扫controller包Service和DAO只扫对应的包。这个坑排查起来费时间但理解之后父子容器的概念就彻底通了。4.3 实操心得与避坑建议第一条先跑通再优化。不要一上来就搞注解驱动、Java Config先用XML把整个流程跑通看到页面能正常显示数据了再去替换配置方式。这样出问题的时候你知道是哪一步引入的。第二条日志级别调到DEBUG。Spring启动时的DEBUG日志会告诉你每个Bean是怎么创建的、每个请求是怎么映射的。遇到NoHandlerFoundException时DEBUG日志会列出所有已注册的HandlerMapping一眼就能看出URL写错在哪。第三条JdbcTemplate的queryForObject慎用。这个方法在结果为空时会抛EmptyResultDataAccessException而不是返回null。如果你不确定查询结果是否一定存在用query方法返回List再判断或者捕获异常。我一般用query方法代码稍微多一行但省去了异常处理的麻烦。第四条Transactional不要加在Controller上。事务应该加在Service层因为一个Service方法可能调用多个DAO方法事务边界应该以业务方法为单位。加在Controller上会导致事务范围过大而且Controller在子容器事务管理器在父容器代理可能不生效。第五条JSP页面记得加isELIgnoredfalse。某些Servlet版本默认忽略EL表达式导致${user.username}直接原样输出。在JSP页面顶部加上% page isELIgnoredfalse %就能解决。4.4 请求流程的完整验证方法搭好之后怎么验证整个流程是通的我的做法是在每个关键节点打日志在DispatcherServlet的URL上发请求看Tomcat访问日志是否有记录。在Controller方法入口打日志确认请求到达了Controller。在Service方法入口打日志确认Controller调用了Service。在DAO方法入口打日志确认Service调用了DAO。在JSP页面输出数据确认Model中的数据正确传递到了视图。这五步走下来任何一个环节断了都能立刻定位。比盲目看代码高效得多。4.5 从练习到实战的扩展方向这个练习跑通之后可以往几个方向扩展一是把JdbcTemplate换成MyBatis体会ORM框架和Spring的整合方式二是加上拦截器做登录校验理解HandlerInterceptor的执行时机三是引入RESTful风格的接口用ResponseBody返回JSON为前后端分离打基础。我个人觉得最有价值的扩展是加拦截器。因为拦截器的执行顺序、与过滤器的区别、与AOP的关系这些知识点在面试中经常被问到而只有真正在项目里配过、调过才能讲清楚。最后分享一个我在实际搭建中养成的习惯每完成一个配置环节就立刻启动Tomcat验证一次不要等所有配置都写完再启动。因为配置错误往往会级联报错一次性启动失败时你面对的可能是一堆错误信息排查成本成倍增加。分步验证虽然启动次数多但每次出问题都只跟刚改的那部分有关定位起来快得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑