☰
SSM框架整合最小可运行工程:XML配置+双容器+JSON响应
2026/10/15 15:24:15 网站建设 项目流程

简介:本资源是一套完整的SSM(Spring+SpringMVC+Mybatis)框架整合入门级实战示例,面向Java Web开发初学者、高校课程实践者及转岗学习者,旨在解决三大主流框架协同配置难、项目结构不清晰、整合流程易出错等典型痛点。压缩包共46个文件,涵盖12个核心XML配置文件(如applicationContext.xml、mybatis-config.xml、servlet-context.xml等)、6个Java源码(含Controller、Service、Mapper接口及实现类)、6个JSP视图页面、6个properties配置文件(数据库与编码设置),以及log、project、classpath等IDE工程元数据,整体仅34KB,轻量易导入,结构规范,可直接在Eclipse/IDEA中运行验证。已有19252人学习下载,是CSDN高热度SSM整合参考样本。读者可获得开箱即用的可运行工程、分层清晰的MVC+DAO目录结构、各框架配置要点的具象化体现,以及HNUST风格的标准化Maven Web项目组织方式,便于快速理解依赖注入、请求分发、SQL映射等关键机制。

1. SSM框架整合不是“拼积木”:一个能跑通登录+查库+返回JSON的最小可验证工程,专治配置失焦与启动报错

你是不是也经历过:网上搜“SSM整合”,下载十几个Demo,解压后不是缺jar包就是web.xml配错,Tomcat一启动就报NoClassDefFoundError或DispatcherServlet no mapping for GET /login?更玄学的是——明明代码一模一样,别人能跑,你本地404。这不是玄学,是SSM整合里藏着三道隐形门槛:类加载顺序冲突、Spring与SpringMVC容器嵌套关系错位、MyBatis SqlSessionFactory注入时机偏差。这个资源不是教学PPT,而是一个经过真实IDEA+Tomcat9+JDK8环境反复锤炼的最小可运行工程:它只做三件事——接收/login POST请求、查user表、返回JSON响应。没有冗余模块,不依赖Maven私服,所有jar包已打包容器内,连log4j.properties都调好了日志级别。适合两类人:刚学完三大框架单点知识、卡在“整合”这一步的新手;以及需要快速搭个后台原型、拒绝被XML配置绕晕的实战派。它不教你IOC原理,但能让你第一次看到控制台打印出{"code":200,"msg":"success","data":{"id":1,"name":"admin"}}时,真正理解什么叫“整合成功”。


2. 为什么选XML而非注解驱动:从容器生命周期看SSM分层治理的底层逻辑

SSM整合的本质,不是把三个框架塞进一个项目,而是让它们按严格时序协作:Spring管理Service/DAO层Bean,SpringMVC接管Web层请求,MyBatis专注SQL执行。XML配置虽显笨重,却是最透明暴露容器边界的方式。注解(如@Controller、@Service)容易掩盖关键矛盾——比如ContextLoaderListener加载的根容器(Spring)和DispatcherServlet加载的子容器(SpringMVC)之间,Bean能否跨容器引用?这个资源坚持用XML,正是为了把这种分层关系钉死在配置文件里,避免新手陷入“为什么@Service注解的类在Controller里@Autowired失败”的黑洞。

2.1 根容器(Spring)与子容器(SpringMVC)的物理隔离设计

根容器由ContextLoaderListener加载,负责初始化Service、DAO、DataSource等非Web组件;子容器由DispatcherServlet加载,只管Controller、HandlerMapping等Web相关Bean。二者是父子关系,子容器可访问父容器Bean,反之不行。本工程通过两个XML文件实现物理隔离:

  • applicationContext.xml(根容器):定义数据源、SqlSessionFactory、MapperScannerConfigurer、Service实现类
  • springmvc-servlet.xml(子容器):定义Controller、ViewResolver、HandlerMapping

提示:web.xml中contextConfigLocation参数指定根容器配置路径,而DispatcherServlet的init-param中contextConfigLocation指定子容器配置路径——这是双容器启动的铁律,漏写任意一个都会导致容器未加载。

2.2 MyBatis核心三件套的装配时序与依赖链

MyBatis在SSM中不是独立存在,它必须被Spring接管才能享受事务管理。本工程的装配链路如下(按加载顺序):

  1. dataSource(DBCP连接池):由Spring创建,作为后续所有数据库操作的基础
  2. sqlSessionFactory:依赖dataSource,并注入configLocation(mybatis-config.xml)和mapperLocations(映射文件路径)
  3. mapperScannerConfigurer:不创建Bean,而是扫描basePackage下所有接口,为每个Mapper接口生成代理Bean,并注入到Spring容器

关键参数说明:

<!-- applicationContext.xml --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <!-- 指向mybatis全局配置,此处仅开启驼峰转换 --> <property name="configLocation" value="classpath:mybatis-config.xml"/> <!-- 扫描Mapper XML文件,注意路径通配符 --> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!-- 必须指定,否则无法生成Mapper代理Bean --> <property name="basePackage" value="com.example.dao"/> <!-- 指定SqlSessionFactory Bean名,建立依赖 --> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean>

mapperLocations值必须是classpath:开头的路径,且*.xml需匹配实际文件名(如UserMapper.xml)。若写成classpath:mapper/UserMapper.xml则只能加载单个文件,失去扫描能力;若路径错误,启动时不会报错,但Mapper接口调用时抛NoSuchBeanDefinitionException——这是静默失败的典型坑。

2.3 SpringMVC DispatcherServlet的初始化陷阱:ContextRefreshedEvent监听失效的根源

DispatcherServlet初始化时会发布ContextRefreshedEvent事件,常被用于预热缓存或加载配置。但在SSM双容器结构下,该事件会被两个容器各发一次:根容器刷新时发一次,子容器刷新时再发一次。若监听器未区分容器,可能重复执行。本工程在LoginController中不依赖事件,而是采用@PostConstruct方法做轻量初始化,规避此问题:

@Controller public class LoginController { @Autowired private UserService userService; // 此方法在Controller Bean初始化后立即执行,仅触发一次 @PostConstruct public void init() { System.out.println("LoginController initialized, ready to handle /login"); } @RequestMapping(value = "/login", method = RequestMethod.POST) @ResponseBody public ResultVO login(@RequestBody User user) { // 实际业务逻辑 return userService.login(user); } }

@PostConstruct由Spring容器保证在依赖注入完成后、Bean可用前执行,比事件监听更可控。若强行用ApplicationRunner,需注意其run()方法在所有容器刷新完毕后才执行,此时Controller可能尚未完成映射注册,导致/login路径不可达。


3. Web层配置实操:从web.xml到RESTful JSON响应的全链路打通

SSM的Web入口是web.xml,它像总开关控制着整个请求生命周期。本工程的web.xml仅保留5个核心配置项,砍掉所有非必要声明,直击痛点:URL映射、字符编码、静态资源放行、容器加载顺序。每一步都对应一个真实翻车场景。

3.1 web.xml四要素:确保DispatcherServlet接管所有请求

标准web.xml需声明四个关键组件,缺一不可:

组件作用本工程配置值常见错误
context-param指定根容器配置路径classpath:applicationContext.xml路径写错成/WEB-INF/applicationContext.xml(实际在classes下)
listener启动根容器org.springframework.web.context.ContextLoaderListener漏写<listener-class>标签,导致Spring容器未启动
servlet定义SpringMVC前端控制器org.springframework.web.servlet.DispatcherServletservlet-name与init-param中contextConfigLocation不一致
servlet-mappingURL模式映射/(覆盖所有请求)映射为*.do导致RESTful风格URL(如/login)不被拦截
<!-- web.xml --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 子容器配置路径,必须与servlet-name一致 --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc-servlet.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <!-- 关键!/ 表示拦截所有请求,包括静态资源 --> <url-pattern>/</url-pattern> </servlet-mapping>

<load-on-startup>1</load-on-startup>确保DispatcherServlet在应用启动时立即初始化,避免首次请求时因加载耗时导致超时。若设为负数或不写,首次访问/login会明显卡顿。

3.2 静态资源放行:解决CSS/JS/IMG 404的终极方案

当<url-pattern>/</url-pattern>时,DispatcherServlet会拦截所有请求,包括/static/css/app.css。若不放行,浏览器控制台将刷屏404。本工程采用SpringMVC原生方案,在springmvc-servlet.xml中声明:

<!-- springmvc-servlet.xml --> <!-- 放行/static/下的所有静态资源 --> <mvc:resources mapping="/static/**" location="/static/" /> <!-- 放行/favicon.ico --> <mvc:resources mapping="/favicon.ico" location="/favicon.ico" /> <!-- 启用注解驱动,必须放在resources之后 --> <mvc:annotation-driven />

<mvc:resources>本质是注册了一个SimpleUrlHandlerMapping,优先级高于RequestMappingHandlerMapping。其mapping属性是请求路径模式,location是WebContent下实际目录(注意末尾斜杠)。若location写成/static(无斜杠),会导致路径解析失败;若mapping写成/static/*(单星号),则无法匹配多级路径如/static/js/lib/jquery.js(需双星号**)。

3.3 中文乱码与JSON响应:@ResponseBody背后的HttpMessageConverter链

@ResponseBody不是魔法,它依赖Spring内置的HttpMessageConverter链将Java对象转为HTTP响应体。本工程默认启用MappingJackson2HttpMessageConverter(Jackson 2.x),但需两处配合:

  1. web.xml中配置字符编码过滤器(解决POST中文参数乱码):
<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

forceEncoding=true强制请求和响应都使用UTF-8,避免Tomcat默认ISO-8859-1干扰。

  1. Controller方法返回类型必须为具体对象(非String):
// ✅ 正确:Spring自动调用Jackson序列化 @RequestMapping("/login") @ResponseBody public ResultVO login(@RequestBody User user) { ... } // ❌ 错误:返回String会被当成纯文本,不走JSON转换器 @RequestMapping("/login") @ResponseBody public String login(@RequestBody User user) { return "{\"code\":200}"; // 浏览器收到的是字符串,不是JSON对象 }

@RequestBody要求前端发送Content-Type: application/json,否则415错误;@ResponseBody则要求返回值是POJO,Spring才会调用MappingJackson2HttpMessageConverter将其序列化为JSON字节流。


4. 避坑:SSM整合中五个血泪教训,每一条都来自真实重启十次Tomcat的经历

SSM整合的坑不在代码,而在配置的毫米级偏差。以下五条是某开发者在搭建第7个SSM工程时,用重启Tomcat的次数换来的结论,现象、原因、解法全部实测有效。

4.1 现象:Tomcat启动成功,但访问/login返回404

原因:DispatcherServlet的<servlet-mapping>未覆盖该路径,或<servlet-name>与servlet定义不一致。
解决:检查web.xml中servlet的<servlet-name>(如springmvc)是否与servlet-mapping中的<servlet-name>完全一致(大小写敏感);确认<url-pattern>为/而非/app/*等子路径。

4.2 现象:控制台打印Could not open ServletContext resource [/WEB-INF/springmvc-servlet.xml]

原因:DispatcherServlet默认查找/WEB-INF/{servlet-name}-servlet.xml,但本工程将配置放在classpath:下。
解决:在servlet的<init-param>中显式指定contextConfigLocation,值为classpath:springmvc-servlet.xml,覆盖默认行为。

4.3 现象:UserService注入成功,但调用userMapper.selectById()抛NullPointerException

原因:MapperScannerConfigurer未正确配置sqlSessionFactoryBeanName,导致Mapper接口未生成代理Bean,注入的是null。
解决:检查applicationContext.xml中MapperScannerConfigurer的<property name="sqlSessionFactoryBeanName">值是否与sqlSessionFactory的id完全一致(如sqlSessionFactory);确认basePackage指向正确的DAO接口包(如com.example.dao)。

4.4 现象:MySQL连接成功,但执行SQL时报Invalid bound statement (not found): com.example.dao.UserMapper.selectById

原因:sqlSessionFactory的mapperLocations路径未匹配到UserMapper.xml文件,或XML中namespace与接口全限定名不一致。
解决:确认UserMapper.xml位于src/main/resources/mapper/目录下;检查XML首行<mapper namespace="com.example.dao.UserMapper">是否与接口类名完全一致;在IDEA中右键mapper文件夹→Mark Directory as→Resources Root,确保编译后文件在classes/mapper/下。

4.5 现象:POST请求返回HTTP Status 415 – Unsupported Media Type

原因:前端未设置Content-Type: application/json,或SpringMVC未启用@RequestBody支持。
解决:前端AJAX请求中添加headers: {'Content-Type': 'application/json'};检查springmvc-servlet.xml中是否启用<mvc:annotation-driven />(它会自动注册MappingJackson2HttpMessageConverter);确认pom.xml中jackson-databind版本与Spring 4.x兼容(本工程用2.9.10)。


5. 数据库与事务:从Druid连接池到@Transactional失效的边界条件

SSM中事务管理是Spring的招牌功能,但@Transactional不是银弹。本工程采用Druid连接池替代DBCP,不仅因性能提升,更因Druid提供了SQL监控和连接泄漏检测——这些能力在排查事务失效时是救命稻草。事务生效有三个硬性前提:代理对象调用、同一容器Bean、非private/final方法。任何一条不满足,@Transactional就形同虚设。

5.1 Druid连接池配置:比DBCP多出的三重防护

Druid在applicationContext.xml中配置如下,相比DBCP增加了监控和防御能力:

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/ssm_demo?useUnicode=true&amp;characterEncoding=UTF-8"/> <property name="username" value="root"/> <property name="password" value="123456"/> <!-- 连接池基础参数 --> <property name="initialSize" value="5"/> <property name="minIdle" value="5"/> <property name="maxActive" value="20"/> <!-- 防御性配置:防止连接泄漏 --> <property name="removeAbandonedOnBorrow" value="true"/> <property name="removeAbandonedOnReturn" value="true"/> <property name="removeAbandonedTimeoutMillis" value="60000"/> <!-- SQL监控:访问/druid可查看实时SQL --> <property name="filters" value="stat,wall"/> </bean>

removeAbandonedOnBorrow=true表示当从连接池获取连接时,若发现有连接超过removeAbandonedTimeoutMillis(60秒)未归还,则强制回收并记录警告。这能快速定位未关闭Connection的代码。filters="stat,wall"开启Druid监控页面(/druid)和防火墙(防SQL注入),是调试阶段的必备开关。

5.2 @Transactional失效的四大雷区与验证方法

@Transactional标注在Service方法上,但以下情况会导致事务不生效:

雷区示例验证方法
自调用userService.login()内部调用this.updateLastLoginTime()(非代理对象)在updateLastLoginTime方法中抛异常,观察数据库是否回滚
异常类型方法抛出RuntimeException外的异常(如Exception)将throws Exception改为throws RuntimeException,或在@Transactional中加rollbackFor=Exception.class
传播行为@Transactional(propagation = Propagation.SUPPORTS)遇到异常不回滚查看方法上propagation属性,确保为REQUIRED(默认)
非public方法@Transactional标注在private方法上直接删除该注解,Spring会忽略它,无任何提示

本工程UserService.login()方法为public,抛出RuntimeException,且未自调用其他事务方法,因此事务100%生效。验证方式:在login方法末尾手动抛new RuntimeException("test rollback"),观察数据库user表last_login_time字段是否未更新。

5.3 事务边界的可视化:Druid监控页里的SQL执行链

Druid监控页(http://localhost:8080/druid)是SSM事务调试的黑匣子。登录后进入SQL监控页,可看到每次请求对应的完整SQL链:

  • 第一行:SELECT * FROM user WHERE username = ?(查询用户)
  • 第二行:UPDATE user SET last_login_time = ? WHERE id = ?(更新登录时间)
  • 若事务生效,这两条SQL会在同一Connection ID下显示,且Execute Time接近;若第二条未出现,说明事务在第一条后已提交或回滚。

更关键的是Active Connection页:若看到连接数持续增长不降,说明有Connection未关闭,大概率是try-catch中漏写了conn.close()——而本工程用MyBatis,此问题由框架自动处理,Druid监控页成了验证框架是否正常工作的仪表盘。


6. 从可运行到可交付:如何用这个SSM工程快速生成你的业务模块

这个SSM工程的价值,不在于它实现了登录,而在于它提供了一套可复制的“模块生成器”。我把它拆成四个原子操作:增表、增Mapper、增Service、增Controller。只要按顺序执行,5分钟内就能为新业务表生成完整CRUD,且保证事务、日志、JSON响应全部就绪。下面以新增order表为例,演示真实工作流。

6.1 原子操作一:数据库建表与MyBatis映射文件生成

先在MySQL中执行建表SQL:

CREATE TABLE `order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `amount` decimal(10,2) DEFAULT '0.00', `status` varchar(20) DEFAULT 'pending', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

然后生成OrderMapper.xml,放在src/main/resources/mapper/下:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.dao.OrderMapper"> <resultMap id="BaseResultMap" type="com.example.entity.Order"> <id column="id" property="id" jdbcType="BIGINT"/> <result column="user_id" property="userId" jdbcType="BIGINT"/> <result column="amount" property="amount" jdbcType="DECIMAL"/> <result column="status" property="status" jdbcType="VARCHAR"/> </resultMap> <select id="selectById" resultMap="BaseResultMap"> SELECT id, user_id, amount, status FROM `order` WHERE id = #{id} </select> <insert id="insert" parameterType="com.example.entity.Order" useGeneratedKeys="true" keyProperty="id"> INSERT INTO `order` (user_id, amount, status) VALUES (#{userId}, #{amount}, #{status}) </insert> </mapper>

注意:namespace必须与DAO接口全限定名一致;<insert>中useGeneratedKeys="true"让MyBatis自动获取自增主键,避免手动SELECT LAST_INSERT_ID()。

6.2 原子操作二:实体类与DAO接口的标准化定义

创建Order.java(src/main/java/com/example/entity/):

package com.example.entity; import java.math.BigDecimal; public class Order { private Long id; private Long userId; private BigDecimal amount; private String status; // getter/setter省略 }

创建OrderMapper.java(src/main/java/com/example/dao/):

package com.example.dao; import com.example.entity.Order; public interface OrderMapper { Order selectById(Long id); int insert(Order order); }

关键:接口名(OrderMapper)必须与OrderMapper.xml的namespace后缀一致;方法名(selectById)必须与XML中<select id="selectById">完全一致。

6.3 原子操作三:Service层的事务封装与异常规范

创建OrderService.java(src/main/java/com/example/service/):

package com.example.service; import com.example.dao.OrderMapper; import com.example.entity.Order; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Transactional(rollbackFor = Exception.class) public Long createOrder(Order order) throws Exception { if (order.getAmount().compareTo(new BigDecimal("0")) <= 0) { throw new IllegalArgumentException("订单金额必须大于0"); } orderMapper.insert(order); return order.getId(); // 返回自增主键 } }

@Transactional(rollbackFor = Exception.class)确保所有Exception及其子类都触发回滚,不局限于RuntimeException;createOrder方法抛出Exception,强制调用方处理业务异常。

6.4 原子操作四:Controller的RESTful路由与统一响应包装

创建OrderController.java(src/main/java/com/example/controller/):

package com.example.controller; import com.example.entity.Order; import com.example.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/order") public class OrderController { @Autowired private OrderService orderService; @PostMapping public ResultVO<Long> create(@RequestBody Order order) { try { Long orderId = orderService.createOrder(order); return ResultVO.success(orderId); } catch (IllegalArgumentException e) { return ResultVO.fail(400, e.getMessage()); } catch (Exception e) { return ResultVO.fail(500, "系统繁忙,请稍后重试"); } } }

@RestController=@Controller + @ResponseBody,省去重复标注;@RequestMapping("/api/order")统一前缀,符合RESTful规范;ResultVO是本工程封装的统一响应体,含code、msg、data三字段,前端无需判断status字段。

从那以后我每次接到新需求,都强制走一遍这四步:建表→写Mapper→写Service→写Controller。不再纠结“SpringMVC怎么配”“MyBatis怎么连”,因为骨架已焊死,血肉可速生。这个SSM工程不是终点,而是你所有Java Web项目的起手式——它不教你怎么造轮子,但确保你每次拧螺丝,都用对了扳手型号。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询