SSM框架深度解析:Spring MVC工作流程、MyBatis缓存与三级缓存实战
1. 项目概述:为什么SSM依然是Java开发者的必修课?
在Java企业级开发领域,SSM框架组合(Spring + Spring MVC + MyBatis)是一个绕不开的经典技术栈。即便在Spring Boot大行其道的今天,深入理解SSM依然是每一位Java开发者夯实基础、理解框架演进脉络的关键。很多面试官喜欢问Spring MVC的工作流程、MyBatis的一二级缓存,或者Spring的IoC原理,其根源就在于SSM代表了Java Web开发从“配置地狱”走向“约定优于配置”的中间态,理解了它,你才能明白Spring Boot为何要那样设计,也才能更从容地应对那些看似“过时”却直指核心的八股文问题。
简单来说,SSM是一个整合框架,它试图在保持灵活性的同时,提供一套相对标准化的Web应用开发方案。Spring作为核心容器,负责管理所有Java对象(Bean)的生命周期和依赖关系;Spring MVC基于Spring,专门处理Web层的请求分发和视图渲染;而MyBatis则作为数据持久层框架,负责将Java对象与数据库记录进行映射。这个组合在几年前几乎是Java Web后台开发的“标配”,无数电商、管理、金融系统都构建于其上。因此,无论是为了维护历史项目,还是为了打牢基础以学习更现代的架构,吃透SSM都至关重要。
2. SSM框架核心组件深度拆解
2.1 Spring:不只是IoC容器,更是生态基石
很多人对Spring的理解停留在“一个通过XML配置Bean的IoC容器”,这其实大大低估了它的价值。Spring的核心设计思想是依赖注入(DI)和面向切面编程(AOP),它通过这两个利器,实现了对象间解耦和横切关注点(如日志、事务)的集中管理。
IoC容器的运作机制:你可以把Spring容器想象成一个超级工厂。在传统开发中,对象A需要对象B,得自己new B()。而在Spring里,你只需要告诉容器:“我需要一个B类型的对象”,容器就会在适当的时机(比如A被创建时)把创建好的B“注入”给A。这个“告诉”的方式,早期是XML,后来是注解(如@Autowired)。容器内部维护着一个巨大的Map,存放着所有Bean的定义和实例。
三级缓存解决循环依赖:这是Spring面试的经典问题。假设A依赖B,B也依赖A,Spring如何解决这个“先有鸡还是先有蛋”的问题?它依靠的是三级缓存:
- 一级缓存(singletonObjects):存放已经完全初始化好的单例Bean。这是最终取Bean的地方。
- 二级缓存(earlySingletonObjects):存放早期暴露的Bean(已实例化,但未完成属性填充和初始化)。
- 三级缓存(singletonFactories):存放Bean的工厂对象,用于生成早期引用。
当创建A时,实例化后(此时还是一个“半成品”),Spring会将其工厂放入三级缓存。接着开始为A注入属性B,发现B不存在,则去创建B。创建B的过程中,又需要注入属性A,此时Spring会从三级缓存中找到A的工厂,生成一个A的早期代理对象(可能是通过AOP创建的代理),并将其放入二级缓存,然后把这个早期引用注入给B。B创建完成后,再注入回A,A得以完成初始化,最终移入一级缓存,并清理二、三级缓存。这个过程精妙地避免了死锁,是理解Spring Bean生命周期和AOP代理创建时机的最佳案例。
注意:三级缓存主要为了解决单例Bean的属性注入循环依赖。对于构造器注入的循环依赖,Spring是无能为力的,会在启动时直接抛出
BeanCurrentlyInCreationException异常。
2.2 Spring MVC:请求是如何被处理的?
Spring MVC是一个基于DispatcherServlet(前端控制器)的MVC框架。它的工作流程是面试必考题,但死记硬背流程图没用,关键要理解每个环节的意图和可扩展点。
一个请求的完整旅程:
- 用户发起请求:浏览器发出
http://localhost:8080/app/user/list。 - DispatcherServlet拦截:在
web.xml中配置的DispatcherServlet会拦截所有请求(通常配置为/)。 - HandlerMapping查找处理器:
DispatcherServlet询问所有注册的HandlerMapping:“这个URL该由哪个方法(Handler)处理?” 最常用的是@RequestMapping注解映射。 - HandlerAdapter执行处理器:找到Handler后,
DispatcherServlet再找一个能执行它的HandlerAdapter(例如,支持@RequestMapping注解的RequestMappingHandlerAdapter)。这个适配器会负责调用目标方法,并在调用前后进行参数解析、数据绑定、模型填充等工作。 - 执行Controller方法:你的
@Controller类中的方法被调用,处理业务逻辑,并返回一个逻辑视图名(如"success")或一个ModelAndView对象。 - ViewResolver解析视图:
DispatcherServlet拿到视图名后,交给ViewResolver(视图解析器)去解析成具体的View对象(如JSP、Thymeleaf模板)。 - 视图渲染:
View对象负责渲染,将模型数据填充到视图中,生成最终的HTML。 - 返回响应:将渲染结果通过
HttpServletResponse返回给用户。
拦截器(Interceptor)的应用:这是Spring MVC提供的强大扩展点,类似于Filter,但更贴近Spring生态。你可以在三个时点插入自定义逻辑:
preHandle:Controller方法执行前。常用于权限校验、日志记录。返回false则中断流程。postHandle:Controller方法执行后,视图渲染前。可以对模型数据进行二次处理。afterCompletion:整个请求完成后。常用于资源清理、性能监控。
例如,一个简单的登录校验拦截器:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); Object user = session.getAttribute("currentUser"); if (user == null) { response.sendRedirect("/login"); return false; // 中断请求 } return true; // 放行 } }然后在Spring MVC配置中注册这个拦截器,并指定它拦截的路径。
2.3 MyBatis:SQL与Java对象的优雅映射
MyBatis的核心优势在于它完美平衡了灵活性和便捷性。与Hibernate这种全自动ORM框架不同,MyBatis要求开发者自己编写SQL,但它负责将SQL执行结果与Java对象(POJO)进行自动映射。
核心组件与工作流程:
- SqlSessionFactory:这是MyBatis的“发动机”,通过读取全局配置文件(
mybatis-config.xml)和所有Mapper XML文件来构建。一个应用通常只有一个。 - SqlSession:代表一次数据库会话,通过
SqlSessionFactory创建。它提供了执行SQL、获取Mapper接口代理对象的方法。重要:它不是线程安全的,每次请求都应该创建新的,用完后必须关闭。 - Mapper接口与XML文件:这是MyBatis的灵魂。你定义一个Java接口(如
UserMapper),然后在同名的XML文件中编写具体的SQL语句。MyBatis会在运行时为这个接口生成动态代理对象,将接口方法的调用映射到XML中对应ID的SQL语句上。
一级缓存与二级缓存:
- 一级缓存(本地缓存):默认开启,作用域是
SqlSession。在同一个SqlSession中,执行两次相同的查询,第二次会直接从缓存中取数据。这里有一个大坑:在开启事务的情况下,如果你在同一个事务中先更新了某条数据,紧接着又查询它,MyBatis为了保证数据一致性,会清空一级缓存,导致你查不到刚刚更新的数据(实际上会去数据库查,但开发者可能误以为缓存没更新)。这是“开启事务后mybatis一级缓存导致数据查询不到”问题的根源。解决方案通常是:1. 关闭一级缓存(不推荐);2. 在更新后手动调用sqlSession.clearCache();3. 设计上避免在同一个短事务中既更新又查询同一数据。 - 二级缓存:作用域是
Mapper(Namespace)级别,可以被多个SqlSession共享。需要手动在XML中配置<cache/>标签。它的实现机制更复杂,涉及序列化/反序列化,在分布式环境下容易产生数据一致性问题,使用需谨慎。
动态SQL:这是MyBatis非常强大的特性,让你能在XML中编写灵活的SQL。常用标签有:
<if>:条件判断。<choose>/<when>/<otherwise>:类似Java的switch-case。<foreach>:遍历集合,常用于IN查询或批量插入。<where>、<set>:智能地处理WHERE和SET子句,避免SQL语法错误(如多余的AND或逗号)。
例如,一个多条件查询:
<select id="selectUsers" resultType="User"> SELECT * FROM user <where> <if test="name != null and name != ''"> AND name like concat('%', #{name}, '%') </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY id DESC </select>3. SSM整合实战:从零搭建一个后台管理系统
理解了理论,我们动手整合一个最简单的SSM项目,实现用户的增删改查(CRUD)。这里使用Maven管理依赖,IDE以IntelliJ IDEA为例。
3.1 环境准备与项目结构
1. 创建Maven Web项目: 在IDEA中选择创建Maven项目,选择maven-archetype-webapp原型。创建完成后,手动补全标准的src/main/java和src/test/java目录。
2. 核心依赖(pom.xml): 你需要引入Spring核心、Spring MVC、MyBatis以及它们的整合包、数据库驱动、连接池等。
<properties> <spring.version>5.3.23</spring.version> <mybatis.version>3.5.11</mybatis.version> <mybatis-spring.version>2.0.7</mybatis-spring.version> </properties> <dependencies> <!-- Spring Core --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> <scope>test</scope> </dependency> <!-- MyBatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>${mybatis-spring.version}</version> </dependency> <!-- 数据库 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.16</version> </dependency> <!-- Servlet API --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- 日志 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.36</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.11</version> </dependency> <!-- 工具 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.14.2</version> </dependency> </dependencies>3. 项目结构:
src/main/java com.example.demo controller/ -- 控制层 service/ -- 业务层接口 service/impl/ -- 业务层实现 dao/ -- 数据访问层接口(Mapper接口) entity/ -- 实体类(POJO) config/ -- Java配置类(可选,替代XML) src/main/resources spring/ -- Spring配置文件 mybatis/ -- MyBatis映射文件 db.properties -- 数据库连接属性 logback.xml -- 日志配置 src/main/webapp WEB-INF web.xml -- 部署描述符 views/ -- JSP页面 static/ -- 静态资源3.2 配置文件的编写与整合
1. web.xml - 配置DispatcherServlet和ContextLoaderListener: 这是SSM整合的入口。DispatcherServlet加载Spring MVC的配置,ContextLoaderListener加载Spring根容器的配置(主要管Service、DAO等)。
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!-- 配置Spring根容器(父容器) --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/applicationContext-*.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- 配置Spring MVC的前端控制器DispatcherServlet(子容器) --> <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <!-- 字符编码过滤器 --> <filter> <filter-name>characterEncodingFilter</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>characterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>2. Spring根容器配置 (applicationContext.xml): 这里主要配置数据源、事务管理器、以及MyBatis的SqlSessionFactory。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!-- 1. 扫描Service层组件 --> <context:component-scan base-package="com.example.demo.service"/> <!-- 2. 加载数据库属性文件 --> <context:property-placeholder location="classpath:db.properties"/> <!-- 3. 配置数据源(Druid) --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="initialSize" value="5"/> <property name="minIdle" value="5"/> <property name="maxActive" value="20"/> </bean> <!-- 4. 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!-- 开启注解驱动的事务管理 --> <tx:annotation-driven transaction-manager="transactionManager"/> <!-- 5. 配置MyBatis的SqlSessionFactory --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <!-- 指定MyBatis全局配置文件位置(可选) --> <property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/> <!-- 指定Mapper XML文件的位置 --> <property name="mapperLocations" value="classpath:mybatis/mapper/*.xml"/> <!-- 配置实体类别名包 --> <property name="typeAliasesPackage" value="com.example.demo.entity"/> </bean> <!-- 6. 配置Mapper扫描器,自动创建Mapper接口的代理Bean --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.demo.dao"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean> </beans>3. Spring MVC配置 (spring-mvc.xml): 这里配置控制器扫描、视图解析器、静态资源处理等。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd"> <!-- 1. 扫描Controller层组件 --> <context:component-scan base-package="com.example.demo.controller" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <!-- 2. 开启MVC注解驱动,自动注册RequestMappingHandlerMapping等 --> <mvc:annotation-driven> <!-- 配置消息转换器,解决@ResponseBody返回JSON的中文乱码问题 --> <mvc:message-converters> <bean class="org.springframework.http.converter.StringHttpMessageConverter"> <property name="supportedMediaTypes"> <list> <value>text/html;charset=UTF-8</value> <value>application/json;charset=UTF-8</value> </list> </property> </bean> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/> </mvc:message-converters> </mvc:annotation-driven> <!-- 3. 静态资源处理:不经过DispatcherServlet --> <mvc:default-servlet-handler/> <!-- 4. 配置视图解析器(JSP) --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean> <!-- 5. 配置拦截器(可选) --> <mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/static/**"/> <bean class="com.example.demo.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors> </beans>4. MyBatis全局配置 (mybatis-config.xml): 这个文件不是必须的,很多配置可以整合到Spring里。这里可以放一些全局设置,比如开启驼峰命名映射。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <!-- 开启驼峰命名自动映射:数据库user_name字段自动映射到Java的userName属性 --> <setting name="mapUnderscoreToCamelCase" value="true"/> <!-- 打印查询日志(配合logback) --> <setting name="logImpl" value="SLF4J"/> </settings> </configuration>3.3 编写业务代码:以用户管理为例
1. 实体类 (User.java):
package com.example.demo.entity; import java.util.Date; public class User { private Long id; private String username; private String password; private String email; private Integer status; private Date createTime; // 省略getter/setter和toString方法 }2. Mapper接口 (UserMapper.java):
package com.example.demo.dao; import com.example.demo.entity.User; import org.apache.ibatis.annotations.Param; import java.util.List; public interface UserMapper { int insert(User user); int deleteById(Long id); int update(User user); User selectById(Long id); List<User> selectAll(); // 动态SQL查询示例 List<User> selectByCondition(@Param("username") String username, @Param("status") Integer status); }3. Mapper XML (UserMapper.xml):
<?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.demo.dao.UserMapper"> <resultMap id="BaseResultMap" type="User"> <id column="id" property="id"/> <result column="username" property="username"/> <result column="password" property="password"/> <result column="email" property="email"/> <result column="status" property="status"/> <result column="create_time" property="createTime"/> </resultMap> <insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id"> INSERT INTO user (username, password, email, status, create_time) VALUES (#{username}, #{password}, #{email}, #{status}, #{createTime}) </insert> <select id="selectByCondition" resultMap="BaseResultMap"> SELECT * FROM user <where> <if test="username != null and username != ''"> AND username LIKE CONCAT('%', #{username}, '%') </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY id DESC </select> <!-- 其他基础的CRUD SQL省略 --> </mapper>4. Service层接口与实现:
// UserService.java package com.example.demo.service; import com.example.demo.entity.User; import java.util.List; public interface UserService { boolean addUser(User user); boolean deleteUser(Long id); boolean updateUser(User user); User getUserById(Long id); List<User> getUsersByCondition(String username, Integer status); } // UserServiceImpl.java package com.example.demo.service.impl; import com.example.demo.dao.UserMapper; import com.example.demo.entity.User; import com.example.demo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service @Transactional // 声明式事务管理 public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public boolean addUser(User user) { user.setCreateTime(new Date()); return userMapper.insert(user) > 0; } // 其他方法实现... }5. Controller层:
package com.example.demo.controller; import com.example.demo.entity.User; import com.example.demo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import java.util.List; @Controller @RequestMapping("/user") public class UserController { @Autowired private UserService userService; // 跳转到用户列表页 @GetMapping("/list") public String listUsers(@RequestParam(required = false) String username, @RequestParam(required = false) Integer status, Model model) { List<User> users = userService.getUsersByCondition(username, status); model.addAttribute("users", users); return "user/list"; // 对应 /WEB-INF/views/user/list.jsp } // 处理新增用户的AJAX请求 @PostMapping("/add") @ResponseBody // 返回JSON public Map<String, Object> addUser(@RequestBody User user) { Map<String, Object> result = new HashMap<>(); try { boolean success = userService.addUser(user); result.put("success", success); result.put("message", success ? "添加成功" : "添加失败"); } catch (Exception e) { result.put("success", false); result.put("message", "系统错误: " + e.getMessage()); } return result; } }3.4 部署与运行:IDEA集成Tomcat
- 配置Tomcat:在IDEA中,点击“Add Configuration”,添加一个“Tomcat Server -> Local”。
- 部署工件:在“Deployment”标签页,点击“+”,选择“Artifact”,添加你的项目生成的WAR包或WAR exploded(推荐后者,支持热更新)。
- 配置上下文路径:可以设置为
/或/ssm-demo。 - 解决潜在问题:
- “源发行版 17 需要目标发行版 17”警告:在
pom.xml中配置Maven编译插件,或在IDEA的Project Structure中确保项目的Language level和Modules的Target bytecode version都是17。 - 内存不足(OutOfMemoryError):在Tomcat配置的“VM options”中增加参数,例如:
-Xms512m -Xmx1024m -XX:MaxMetaspaceSize=256m。 - 静态资源404:确保
spring-mvc.xml中配置了<mvc:default-servlet-handler/>或<mvc:resources mapping="/static/**" location="/static/"/>。
- “源发行版 17 需要目标发行版 17”警告:在
- 启动Tomcat,访问
http://localhost:8080/user/list即可看到页面。
4. 进阶话题与生产环境避坑指南
4.1 事务管理:@Transactional的正确使用姿势
在SSM中,事务通常通过Spring的@Transactional注解来声明式管理。但使用不当,事务会失效。
事务失效的常见场景:
- 方法非public:
@Transactional只能用于public方法。 - 自调用问题:在同一个类中,一个非事务方法A调用事务方法B,事务不会生效。因为Spring的事务管理基于AOP代理,自调用不走代理。
- 异常类型不对:默认只对RuntimeException和Error回滚。如果方法抛出了受检异常(Exception),事务不会回滚。需要用
@Transactional(rollbackFor = Exception.class)。 - 数据库引擎不支持:如MySQL的MyISAM引擎不支持事务。
- 多数据源未指定:如果项目配置了多个数据源,需要在
@Transactional中指定transactionManager。
实操建议:
- 将事务注解放在Service层实现类的方法上,而不是接口上(虽然接口上也有效,但Spring团队建议放在具体类上)。
- 明确指定回滚异常:
@Transactional(rollbackFor = Exception.class)。 - 对于只读查询,使用
@Transactional(readOnly = true),数据库可能会做一些优化。
4.2 解决SSM后台空值问题
前端传参到后台,经常遇到空字符串""被映射成null,或者日期格式解析失败的问题。
1. 空字符串转null: Spring MVC在数据绑定时,默认会将空字符串转为null。如果你希望区分“用户没填”和“用户填了空字符串”,可以自定义PropertyEditor或使用@InitBinder在Controller中处理。
@InitBinder public void initBinder(WebDataBinder binder) { // 设置空字符串不转为null binder.registerCustomEditor(String.class, new StringTrimmerEditor(false)); }更常见的做法是,在前端校验时避免提交空字符串,或者在业务逻辑中统一按null处理。
2. 日期类型绑定: 前端传"2023-10-01",后台用Date接收会报错。解决方法:
- 使用
@DateTimeFormat注解:@PostMapping("/add") public String add(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date birthDate) { // ... } - 全局配置:在配置类中实现
WebMvcConfigurer接口,重写addFormatters方法,注册一个全局的日期格式化器。
3. JSON序列化/反序列化问题: 使用@RequestBody接收JSON时,Jackson库负责转换。空值处理可以在配置Jackson时设置:
// 在Spring MVC配置中配置ObjectMapper @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); // 忽略null值字段,不序列化到JSON mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 反序列化时,忽略JSON中存在的未知属性 mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); return mapper; }4.3 性能优化与监控
1. MyBatis SQL监控: 使用mybatis-log-free这类插件,或在日志配置中直接输出MyBatis执行的SQL语句及参数,是调试和性能分析的利器。在logback.xml中配置:
<logger name="com.example.demo.dao" level="DEBUG"/> <!-- 你的Mapper接口包 --> <logger name="java.sql.Connection" level="DEBUG"/> <logger name="java.sql.Statement" level="DEBUG"/> <logger name="java.sql.PreparedStatement" level="DEBUG"/>2. Druid连接池监控: Druid提供了强大的监控功能。在web.xml中配置Druid的StatViewServlet,即可通过/druid/index.html访问监控后台,查看SQL执行情况、连接池状态等。
<servlet> <servlet-name>DruidStatView</servlet-name> <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>DruidStatView</servlet-name> <url-pattern>/druid/*</url-pattern> </servlet-mapping>务必记得配置访问白名单和账号密码,切勿直接暴露在生产环境!
3. 分页查询优化: MyBatis可以使用PageHelper插件实现物理分页。它能自动将查询语句改写为带有LIMIT的分页语句,避免内存分页的性能问题。整合PageHelper后,在Service层只需:
PageHelper.startPage(pageNum, pageSize); // 紧跟在查询语句前 List<User> users = userMapper.selectByCondition(username, status); PageInfo<User> pageInfo = new PageInfo<>(users);4.4 从SSM平滑迁移到Spring Boot的思考
Spring Boot的本质是“约定大于配置”,它内嵌了Tomcat,提供了海量的Starter来简化依赖,并提供了自动配置。如果你已经熟悉SSM,迁移到Spring Boot会非常顺畅。
核心变化:
- 配置方式:从多个XML文件变为一个
application.yml或application.properties文件。 - 依赖管理:从手动管理一堆
<dependency>变为引入一个spring-boot-starter-web(包含了Spring MVC、Tomcat等)。 - MyBatis整合:引入
mybatis-spring-boot-starter,几乎零配置即可使用。 - 部署:从打WAR包部署到外部Tomcat,变为打可执行的JAR包(内嵌Tomcat),通过
java -jar运行。
迁移步骤:
- 创建一个Spring Boot项目。
- 在
pom.xml中添加spring-boot-starter-web和mybatis-spring-boot-starter依赖。 - 将原来的
applicationContext.xml和spring-mvc.xml中的配置,转化为application.yml中的属性或Java配置类(使用@Configuration)。 - 将
UserMapper.xml文件放在src/main/resources/mapper/目录下。 - 在启动类上添加
@MapperScan("com.example.demo.dao")注解。 - 删除
web.xml。
Spring Boot并没有颠覆SSM,而是将其标准化和自动化了。理解SSM的每一个配置项在Spring Boot中是如何被自动配置的,能让你在使用Spring Boot时更有底气,遇到问题也能快速定位。例如,当你需要自定义一个Filter时,在SSM中是在web.xml里配置,在Spring Boot中则是通过一个@Configuration类来注册一个FilterRegistrationBean。其底层原理是相通的。
