
简介本资源是一个面向Java Web初学者的SpringMVC入门实践项目聚焦MVC架构核心原理与基础环境搭建帮助开发者快速掌握控制器配置、请求映射、视图解析及前后端交互流程。压缩包共22个文件含5个XML配置文件如web.xml、pom.xml、spring-mvc.xml等承担框架初始化与依赖管理、4个JSP页面实现视图层展示如hello.jsp、success.jsp、2个Java源码文件含Controller标注的处理器类以及class、prefs、classpath等开发元数据文件整体仅11KB轻量易导入。已有269人学习下载适合Eclipse/STS环境下快速运行验证。读者可直接部署运行“Hello, World!”示例完整观察DispatcherServlet分发流程、HandlerMapping匹配逻辑、ModelAndView数据传递机制并通过目录结构清晰理解SpringMVC标准Web项目组织方式src/main/java、WebContent/WEB-INF、.project等是理解框架启动链路与MVC职责分离的优质教学样本。1. SpringMVC项目一个能跑通、能调试、能改接口的完整Web工程骨架你刚 clone 下来一个标着“SpringMVC项目”的源码包双击 pom.xml 却发现 IDEA 报红——不是缺依赖是缺上下文。它既不是 Spring Boot 的开箱即用也不是纯 Servlet 的裸写而是一套被真实业务项目反复锤炼过的、带完整分层结构的 SpringMVC 落地模板Controller 层有 RESTful 风格接口和参数校验Service 层用 Transactional 管理事务边界DAO 层封装了 MyBatis 的 SqlSessionTemplate 接口式 Mapper连 logback.xml 和 web.xml 的 namespace 都对齐了 Tomcat 9 的规范。这不是教学 Demo而是某高校实验室在部署学生实训平台时从三个不同课程项目里抽离出的共性骨架——它不炫技但每个模块都经得起打断点、查 SQL、压测并发。如果你正卡在“学完 SpringMVC 概念却写不出可部署项目”这道坎上或者需要快速搭一个带登录、数据列表、文件上传的管理后台原型这个包就是你该先跑起来的基准线。2. 项目结构与技术栈解析从 web.xml 到 DispatcherServlet 的链路闭环2.1 目录结构还原为什么 src/main/webapp/WEB-INF 下必须有这 4 个文件项目采用标准 Maven Web 工程结构关键路径如下非全量仅列影响启动的核心src/main/ ├── java/ │ └── com.example.mvc/ # 根包名实际使用时需全局替换 │ ├── controller/ # Controller 注解类处理 HTTP 请求 │ ├── service/ # 接口 实现类Service Transactional │ ├── dao/ # MyBatis Mapper 接口无 XML SqlSessionTemplate 注入 │ └── entity/ # POJO含 Table/ColumnJPA 注解供 MyBatis-Plus 兼容 ├── resources/ │ ├── spring-mvc.xml # DispatcherServlet 加载的核心配置注解扫描、视图解析器、拦截器 │ ├── spring-dao.xml # 数据源 SqlSessionFactoryBean MapperScannerConfigurer │ ├── jdbc.properties # 数据库连接参数默认 H2 内存库开箱即用 │ └── logback.xml # 控制台 文件双输出ERROR 级别自动归档 └── webapp/ ├── WEB-INF/ │ ├── web.xml # Servlet 3.0 配置DispatcherServlet CharacterEncodingFilter │ ├── dispatcher-servlet.xml → 已废弃内容合并至 spring-mvc.xml │ └── views/ # JSP 模板目录login.jsp, list.jsp支持 EL 表达式 └── static/ # CSS/JS/IMG 静态资源未走 Spring MVC由 DefaultServlet 处理提示dispatcher-servlet.xml在本项目中已被显式删除所有 MVC 相关 Bean如InternalResourceViewResolver、RequestMappingHandlerMapping均定义在spring-mvc.xml中。这是为避免 Spring 容器父子上下文混乱——ContextLoaderListener加载根容器service/daoDispatcherServlet加载子容器controller二者通过import resourceclasspath:spring-mvc.xml/显式关联而非依赖默认命名规则。2.2 技术栈版本锁定为什么用 Spring 5.3.x 而非 6.x项目pom.xml中关键依赖版本如下已验证兼容性properties spring.version5.3.31/spring.version !-- 支持 JDK 8~17Tomcat 9.0 -- mybatis.version3.5.13/mybatis.version !-- 与 SqlSessionTemplate 配合稳定 -- mybatis-spring.version2.0.7/mybatis-spring.version !-- 注意非 3.x因 Spring 5.x 不兼容 -- h2.version2.2.224/h2.version !-- 内存库启动快无需安装 -- /properties选择 Spring 5.3.x 是权衡结果Spring 6.x 强制要求 JDK 17 和 Jakarta EE 9javax.*→jakarta.*而本项目web.xml中仍使用javax.servlet.Filter且多数企业级 Tomcat 8.5/9.0 环境尚未升级 Jakarta 命名空间。若强行升级需同步修改所有import javax.servlet.*→jakarta.servlet.*web.xml的xsi:schemaLocation指向 Jakarta NSpom.xml中servlet-api依赖换为jakarta.servlet-api这不是技术落后而是部署确定性优先——你在实训机房或老旧服务器上跑通比在本地 IDE 里炫新特性更重要。2.3 web.xml 启动流程从 Servlet 容器到 Spring 上下文的三次加载web.xml是整个项目的入口契约其加载顺序直接决定 Bean 是否可用!-- 1. 根容器加载 service/dao 层 Bean -- listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-dao.xml, classpath:spring-mvc.xml/param-value /context-param !-- 2. 字符编码过滤器必须在 DispatcherServlet 之前 -- filter filter-nameencodingFilter/filter-name filter-classorg.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-mapping !-- 3. 前端控制器只处理 /app/* 路径静态资源交由容器默认 Servlet -- servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-value/param-value !-- 空值表示使用 classpath:spring-mvc.xml -- /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern/app/*/url-pattern /servlet-mapping逻辑说明ContextLoaderListener创建父 ApplicationContext加载spring-dao.xml数据源、Mapper和spring-mvc.xmlMVC 组件此容器中的 Bean 可被子容器引用CharacterEncodingFilter必须在DispatcherServlet之前注册否则 POST 请求中文参数会乱码request.setCharacterEncoding()必须在request.getParameter()之前调用DispatcherServlet创建子 ApplicationContext它继承父容器的 Bean但可覆盖如自定义HandlerExceptionResolver其url-pattern/app/*/url-pattern意味着所有 Controller 接口需以/app/开头如RequestMapping(/app/user/list)而/static/下的 JS/CSS 自动绕过 Spring由 Tomcat 默认 Servlet 返回。3. 运行与调试从零部署到断点验证请求生命周期3.1 三步启动法IDEA Tomcat 9.0 H2 内存库Step 1导入项目并指定 JDK打开 IDEA → Open → 选择项目根目录 → 选中pom.xml→ Import as projectFile → Project Structure → Project → SDK 设置为 JDK 8 或 JDK 11不可用 JDK 17因 H2 2.2.x 对 JDK 17 支持不稳定Modules → Sources → 确认src/main/java和src/main/resources已标记为 Sources/ResourceStep 2配置 Tomcat ServerRun → Edit Configurations → → Tomcat Server → LocalDeployment → Artifact → 选择springmvc:war explodedServer → JRE → 选择与 Project SDK 一致的 JDKOn Update action → Update classes and resources热更新基础After launch → Open browser → http://localhost:8080/springmvc/app/login 注意路径Step 3启动前检查 H2 控制台可选但强烈推荐jdbc.properties中已配置jdbc.urljdbc:h2:mem:testdb;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE;TRACE_LEVEL_SYSTEM_OUT2启动后访问http://localhost:8080/h2-console填入JDBC URL:jdbc:h2:mem:testdbUser Name:saPassword: 空点击 Connect执行SELECT * FROM user;可见初始化的测试数据SQL 在src/main/resources/data.sql中注意H2 内存库数据随 Tomcat 重启丢失如需持久化请将jdbc.url改为jdbc:h2:./data/testdb并确保./data目录可写。3.2 断点跟踪一个登录请求的完整链路/app/login → 成功跳转我们以LoginController为例实测请求如何流转Controller RequestMapping(/app) public class LoginController { Autowired private UserService userService; // GET /app/login → 返回 login.jsp 页面 GetMapping(/login) public String showLogin() { return login; // 解析为 /WEB-INF/views/login.jsp } // POST /app/login → 处理表单提交 PostMapping(/login) public String doLogin( RequestParam String username, RequestParam String password, Model model) { User user userService.login(username, password); if (user ! null) { model.addAttribute(user, user); return redirect:/app/user/list; // 重定向防止重复提交 } else { model.addAttribute(error, 用户名或密码错误); return login; } } }断点设置位置与预期行为在showLogin()第一行打断点 → 启动后访问http://localhost:8080/springmvc/app/loginIDEA 自动停住F8 单步可见return loginInternalResourceViewResolver将login解析为/WEB-INF/views/login.jsp此时浏览器地址栏仍是/app/loginGET在doLogin()方法入口打断点 → 提交表单后断点触发观察username/password值是否正确若为 null检查CharacterEncodingFilter是否生效userService.login()内部会调用UserMapper.selectByUsernameAndPassword()此处可进到 DAO 层看 SQL 执行日志logback 输出Preparing: SELECT * FROM user WHERE username ? AND password ?若返回user ! nullreturn redirect:/app/user/list触发 302 重定向浏览器地址栏变为/app/user/list此时UserListController的GetMapping(/user/list)被匹配。关键验证点在doLogin()中故意抛出RuntimeException观察SimpleMappingExceptionResolver是否捕获并跳转到error.jsp配置在spring-mvc.xml中。3.3 日志驱动调试logback.xml 如何帮你定位 SQL 和事务问题logback.xml配置了三级日志输出针对性极强!-- 1. 控制台输出只显示 ERROR 和 WARN -- appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender !-- 2. SQL 日志MyBatis 执行的每条语句 -- logger namecom.example.mvc.dao levelDEBUG additivityfalse appender-ref refCONSOLE/ /logger !-- 3. 事务日志查看 Transactional 是否生效 -- logger nameorg.springframework.transaction levelDEBUG additivityfalse appender-ref refCONSOLE/ /logger !-- 4. 根日志INFO 级别避免刷屏 -- root levelINFO appender-ref refCONSOLE/ /root现象与排查启动时若看到Creating shared instance of singleton bean sqlSessionFactory说明 MyBatis 初始化成功执行登录时控制台出现 Preparing: SELECT * FROM user WHERE username ? AND password ?证明 DAO 层调用正常若修改UserService.login()方法在方法开头加TransactionAspectSupport.currentTransactionStatus()日志中出现Creating new transaction说明Transactional生效若UserMapper.updateStatus()抛异常后数据库未回滚检查该方法是否在Service类内部被this.updateStatus()调用代理失效必须改为userService.updateStatus()。4. 常见问题排查5 个血泪经验总结的翻车现场4.1 现象启动报错java.lang.ClassNotFoundException: javax.servlet.Filter原因IDEA 导入 Maven 项目时未自动添加servlet-api依赖或 Tomcat 依赖被设为Provided但未正确关联到 Tomcat Server。解决检查pom.xml是否有dependencygroupIdjavax.servlet/groupIdartifactIdjavax.servlet-api/artifactIdversion4.0.1/versionscopeprovided/scope/dependency在 IDEA 中File → Project Structure → Modules → Dependencies → 点 → Library → Configure → Add → 选择你配置的 Tomcat 9.0 目录下的lib/servlet-api.jar玄学操作右键项目 → Maven → Reload再 Clean → Rebuild。4.2 现象POST 请求参数全为 null但 GET 正常原因CharacterEncodingFilter未生效或web.xml中 filter-mapping 的url-pattern未覆盖/app/*。解决确认filter-mapping的url-pattern是/*不是/app/*因为 Filter 在 Servlet 之前执行需拦截所有请求检查CharacterEncodingFilter的forceEncodingtrue否则仅当 request.getCharacterEncoding()null 时才设置在doLogin()中加System.out.println(request.getCharacterEncoding())应输出UTF-8。4.3 现象JSP 页面 EL 表达式不解析显示${user.username}原文原因web.xml的web-app版本过低如 2.3或jsp-api依赖缺失。解决web.xml第一行必须为?xml version1.0 encodingUTF-8?!DOCTYPE web-app PUBLIC -//Sun Microsystems, Inc.//DTD Web Application 2.3//EN...→改为 3.0 声明web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0pom.xml中添加jsp-api依赖Tomcat 9.0 对应javax.servlet.jsp:jsp-api:2.3.3。4.4 现象H2 控制台能连但SELECT * FROM user报Table USER not found原因data.sql未被执行或schema.sql缺失导致表未创建。解决spring-dao.xml中确认DataSource配置了ddl-autocreateH2 不支持需手动建表src/main/resources/下必须有schema.sql建表语句和data.sql插入语句并在spring-dao.xml中通过init-sql指定bean iddataSource classorg.springframework.jdbc.datasource.DriverManagerDataSource property namedriverClassName valueorg.h2.Driver/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ !-- 关键初始化 SQL -- property nameconnectionInitSqls list valueCREATE TABLE IF NOT EXISTS user(id BIGINT PRIMARY KEY, username VARCHAR(50), password VARCHAR(50))/value valueINSERT INTO user VALUES(1, admin, 123456) ON CONFLICT DO NOTHING/value /list /property /bean4.5 现象Transactional不回滚数据库数据已提交原因异常类型不对默认只对RuntimeException回滚或事务传播行为配置错误。解决在Transactional上显式声明Transactional(rollbackFor Exception.class)检查UserService类是否被Service注解且该类的 Bean 是由 Spring 容器创建非new UserServiceImpl()若updateStatus()方法被同一 Service 内其他方法调用必须通过ApplicationContext.getBean(UserService.class).updateStatus()调用否则 AOP 代理失效。5. 核心模块代码逐段拆解从 Controller 到 DAO 的真实实现5.1 LoginControllerRESTful 风格与传统 Model 返回的混合实践Controller RequestMapping(/app) public class LoginController { Autowired private UserService userService; // 【GET】返回登录页不走 JSON用 JSP 渲染 GetMapping(/login) public String showLogin(Model model) { model.addAttribute(title, 用户登录); // 传给 JSP 的变量 return login; // ViewResolver 解析为 /WEB-INF/views/login.jsp } // 【POST】表单提交返回重定向指令非 JSON PostMapping(/login) public String doLogin( RequestParam String username, // 必填无默认值 RequestParam String password, HttpSession session, // 获取 session 存储用户信息 Model model) { try { User user userService.login(username, password); if (user ! null) { session.setAttribute(currentUser, user); // 登录态存 session return redirect:/app/user/list; // 302 重定向防止 F5 刷新重复提交 } else { model.addAttribute(error, 账号或密码错误); return login; // 返回原页面保留 form 数据 } } catch (Exception e) { model.addAttribute(error, 系统繁忙请稍后再试); return login; } } // 【GET】退出登录清空 session GetMapping(/logout) public String logout(HttpSession session) { session.invalidate(); // 销毁 session return redirect:/app/login; } }参数说明RequestParam强制从 query string 或 form data 中提取requiredtrue默认若允许为空需加requiredfalseHttpSessionSpring MVC 自动注入无需request.getSession()Model轻量级数据容器比ModelMap更简洁底层都是LinkedHashMapredirect:前缀触发Response.sendRedirect()浏览器地址栏变化且后续请求无法访问原model中的数据因重定向是两次独立请求。5.2 UserServiceImpl事务边界与异常转换的典型写法Service public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; // MyBatis 接口无实现类 Override Transactional(rollbackFor Exception.class) public User login(String username, String password) { // 1. 参数校验业务规则 if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { throw new IllegalArgumentException(用户名和密码不能为空); } // 2. 查询数据库MyBatis 自动映射到 User 对象 User user userMapper.selectByUsernameAndPassword(username, password); if (user null) { return null; } // 3. 更新最后登录时间同一事务内 user.setLastLoginTime(new Date()); userMapper.updateLastLoginTime(user.getId(), user.getLastLoginTime()); // 4. 返回脱敏对象不返回 password 字段 user.setPassword(null); return user; } Override Transactional(rollbackFor Exception.class) public int register(User user) { // 插入前检查用户名是否已存在 if (userMapper.selectByUsername(user.getUsername()) ! null) { throw new RuntimeException(用户名已存在); } return userMapper.insert(user); // 返回影响行数 } }关键设计点Transactional加在public方法上保证整个方法原子性rollbackFor Exception.class使checked exception也触发回滚默认只对RuntimeExceptionuser.setPassword(null)是安全习惯避免敏感字段意外泄露到前端即使 JSP 中没写${user.password}JSON 序列化时也可能暴露userMapper是接口Spring 通过MapperScannerConfigurer扫描com.example.mvc.dao包下所有接口动态生成代理类。5.3 UserMapperMyBatis 接口式编程与 SQL 分离Mapper public interface UserMapper { /** * 根据用户名和密码查询用户用于登录 * XML 映射文件UserMapper.xml位于 resources/com/example/mvc/dao/ */ User selectByUsernameAndPassword(Param(username) String username, Param(password) String password); /** * 根据用户名查询用于注册时判重 */ User selectByUsername(Param(username) String username); /** * 插入新用户 */ int insert(User user); /** * 更新最后登录时间只更新指定字段 */ int updateLastLoginTime(Param(id) Long id, Param(lastLoginTime) Date lastLoginTime); }对应UserMapper.xml片段src/main/resources/com/example/mvc/dao/UserMapper.xml?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.mvc.dao.UserMapper !-- 查询语句#{} 防 SQL 注入${} 直接拼接慎用 -- select idselectByUsernameAndPassword resultTypecom.example.mvc.entity.User SELECT id, username, email, last_login_time AS lastLoginTime FROM user WHERE username #{username} AND password #{password} /select select idselectByUsername resultTypecom.example.mvc.entity.User SELECT * FROM user WHERE username #{username} /select insert idinsert parameterTypecom.example.mvc.entity.User useGeneratedKeystrue keyPropertyid INSERT INTO user (username, password, email, create_time) VALUES (#{username}, #{password}, #{email}, NOW()) /insert update idupdateLastLoginTime UPDATE user SET last_login_time #{lastLoginTime} WHERE id #{id} /update /mapper参数说明Param注解当方法有多个参数时必须用Param指定名称否则 MyBatis 无法绑定#{username}resultType指定返回对象类型MyBatis 自动按字段名映射last_login_time→lastLoginTime靠驼峰命名自动转换useGeneratedKeystrue启用主键回填插入后user.getId()可获取数据库生成的 IDkeyPropertyid指定哪个属性接收生成的主键值。6. 二次开发实战3 个高频改造场景与验证方法6.1 场景一将 JSP 页面替换为 Vue 前端前后端分离改造目标保留后端 SpringMVC 接口前端改用 Vue CLI 开发通过 Axios 调用/app/**接口。改造步骤后端 CORS 支持避免跨域在spring-mvc.xml中添加CorsConfigurationBeanmvc:cors mvc:mapping path/app/** allowed-originshttp://localhost:8081 allow-credentialstrue/ /mvc:cors注意allowed-origins不能为*当allow-credentialstrue时必须指定具体域名。统一 JSON 响应格式新增Result.javapublic class ResultT { private int code; // 200 成功500 失败 private String msg; private T data; // getter/setter... }修改LoginController.doLogin()PostMapping(/login) ResponseBody // 关键返回 JSON不走 ViewResolver public ResultUser doLogin(RequestParam String username, RequestParam String password) { try { User user userService.login(username, password); if (user ! null) { return Result.success(user); } else { return Result.fail(用户名或密码错误); } } catch (Exception e) { return Result.fail(系统异常); } }Vue 调用示例src/api/user.jsimport axios from axios export function login(username, password) { return axios.post(/springmvc/app/login, null, { params: { username, password } // GET 参数用 paramsPOST 表单用 data }) }验证方法启动后端访问http://localhost:8080/springmvc/app/login?usernameadminpassword123456返回 JSON启动 Vuenpm run serve在浏览器控制台执行login(admin,123456)检查 Network 面板是否 200 且响应体为{code:200,msg:success,data:{...}}。6.2 场景二增加文件上传功能CommonsMultipartResolver 配置目标在用户管理页增加头像上传保存到WEB-INF/upload/目录。改造步骤添加依赖pom.xmldependency groupIdcommons-fileupload/groupId artifactIdcommons-fileupload/artifactId version1.5/version /dependency配置 MultipartResolverspring-mvc.xmlbean idmultipartResolver classorg.springframework.web.multipart.commons.CommonsMultipartResolver property namemaxUploadSize value10485760/ !-- 10MB -- property namedefaultEncoding valueUTF-8/ property nameuploadTempDir valuefile:///C:/temp// !-- 临时目录 -- /beanController 添加上传接口PostMapping(/user/upload-avatar) ResponseBody public ResultString uploadAvatar(RequestParam(file) MultipartFile file, RequestParam(userId) Long userId) { if (file.isEmpty()) { return Result.fail(文件不能为空); } try { // 保存到 WEB-INF/upload/ 目录相对路径 String fileName userId _ System.currentTimeMillis() _ file.getOriginalFilename(); String uploadPath request.getServletContext().getRealPath(/WEB-INF/upload/); File dir new File(uploadPath); if (!dir.exists()) dir.mkdirs(); file.transferTo(new File(dir, fileName)); return Result.success(/upload/ fileName); // 返回可访问路径 } catch (Exception e) { return Result.fail(上传失败 e.getMessage()); } }验证方法使用 Postman 发送POST /springmvc/app/user/upload-avatarBody → form-data → keyfile选择文件keyuserId填数字检查WEB-INF/upload/目录下是否生成文件访问http://localhost:8080/springmvc/upload/xxx.jpg确认可直接下载。6.3 场景三集成 Redis 缓存用户信息减少 DB 查询目标用户登录成功后将User对象缓存到 Redis后续查询优先读缓存。改造步骤添加依赖pom.xmldependency groupIdorg.springframework.data/groupId artifactIdspring-data-redis/artifactId version2.7.18/version !-- 适配 Spring 5.3.x -- /dependency dependency groupIdredis.clients/groupId artifactIdjedis/artifactId version4.3.2/version /dependencyRedis 配置spring-dao.xmlbean idjedisConnectionFactory classorg.springframework.data.redis.connection.jedis.JedisConnectionFactory property namehostName valuelocalhost/ property nameport value6379/ property namedatabase value0/ /bean bean idredisTemplate classorg.springframework.data.redis.core.RedisTemplate property nameconnectionFactory refjedisConnectionFactory/ property namekeySerializer bean classorg.springframework.data.redis.serializer.StringRedisSerializer/ /property property namevalueSerializer bean classorg.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer/ /property /beanUserService 增加缓存逻辑Service public class UserServiceImpl implements UserService { Autowired private RedisTemplateString, User redisTemplate; Override public User login(String username, String password) { // 1. 先查缓存 String cacheKey user:login: username; User cachedUser redisTemplate.opsForValue().get(cacheKey); if (cachedUser ! null password.equals(cachedUser.getPassword())) { return cachedUser; } // 2. 查 DB User user userMapper.selectByUsernameAndPassword(username, password); if (user ! null) { // 3. 写缓存设置 30 分钟过期 redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES); } return user; } }验证方法启动 Redisredis-server.exe第一次登录时观察日志是否有Preparing: SELECT * FROM user ...DB 查询第二次用相同账号登录日志中不再出现该 SQL且redis-cli执行GET user:login:admin应返回 JSON修改数据库中该用户密码再次登录应失败缓存未更新证明缓存穿透需额外处理如加锁或布隆过滤器。从那以后我每次接手一个老 SpringMVC 项目第一件事就是打开web.xml看ContextLoaderListener和DispatcherServlet的配置顺序再扫一眼spring-dao.xml里有没有init-sql—— 这两处定生死其余都是缝合。遇到ClassNotFoundException我直接去 IDEA 的 Project Structure 里删掉所有Provided依赖再重加一遍 Tomcat lib比查文档快十倍。希望帮到你。本文还有配套的精品资源点击获取