SSH经典架构实战解析:Spring3.2+Struts2.3+MyBatis遗留系统拆解指南 📅 发布时间:2026/9/15 13:03:50 👁 浏览次数: 简介这是一套基于Spring 3.2、Struts2.3、MyBatis与MySQL 5.7开发的隐患排查系统完整源码面向计算机类专业学生及初级Java开发人员适用于课程设计、毕业设计、企业内部管理类项目原型参考与实战学习。资源包含651个文件涵盖113个核心Java业务逻辑与控制器代码、30个XML配置文件含Spring/MyBatis映射与Struts动作定义、27个JSP页面模板、170个CSS样式文件含Bootstrap、EasyUI等主流UI框架、144个PNG图标资源及53个运行依赖JAR包整体压缩包仅28.15MB轻量易部署。已有191人下载学习代码经实测可正常编译运行数据库脚本SQL文件与配置说明齐全目录结构遵循经典SSH分层规范便于理解MVC流程与前后端交互逻辑。读者可直接导入IDE调试快速掌握企业级Web系统从环境搭建、权限控制到隐患数据增删改查的全流程实现。1. 一个跑在 Spring3.2 Struts2.3 上的隐患排查系统为什么现在还值得拆你可能第一反应是Spring3.2Struts2.3这不都是十年前的老古董了吗确实主流项目早已迁移到 Spring Boot 3.x 和 MyBatis-Plus但正因如此这套「隐患排查系统」反而成了极少数能完整呈现经典 SSHSpring Struts Hibernate/MyBatis三层架构落地细节的实战样本。它不是玩具 Demo而是真实业务场景压缩后的可运行实体——登录、角色权限控制、隐患录入、状态流转、导出报表、分页查询全部闭环数据库字段命名规范、DAO 层 SQL 显式分页、Action 中手动校验、Service 层事务边界清晰。对刚学完《Java Web 编程》或《企业级应用开发》课程的学生来说它比 Spring Boot 官方示例更贴近教材逻辑对企业里需要快速理解遗留系统改造路径的工程师而言它提供了从 XML 配置到拦截器链、从 SqlSessionTemplate 到事务传播行为的完整映射线索。尤其当你要接手一个还在用 Struts2 的老系统时这套代码就是最接近生产环境的“活体解剖标本”。2. 架构选型与模块划分为什么是 Spring3.2 Struts2.3 MyBatis 而不是其他组合2.1 技术栈版本锁定背后的工程现实这套系统选择 Spring 3.2.18.RELEASE、Struts 2.3.32、MyBatis 3.2.8、MySQL 5.7.21并非偶然。Spring 3.2 是最后一个支持 Servlet 2.5 容器如 Tomcat 6/7且未强制要求 Java 7 的稳定大版本Struts 2.3 系列则是在 Struts 2.5 推出前漏洞修复最密集、插件生态最成熟的阶段MyBatis 3.2 引入了SelectProvider动态 SQL 支持又尚未引入MapperScannerConfigurer的自动扫描机制——这意味着所有 Mapper 必须显式注册恰好暴露了 IOC 容器如何接管 DAO 层的底层逻辑。MySQL 5.7.21 则是 5.7 分支中默认开启sql_modeSTRICT_TRANS_TABLES的关键版本直接影响INSERT IGNORE和ON DUPLICATE KEY UPDATE的行为而隐患数据录入模块恰恰大量使用了这类语句。提示不要试图强行升级 Spring 版本。Spring 4.x 开始废弃org.springframework.web.struts包而该系统中SpringObjectFactory继承自com.opensymphony.xwork2.ObjectFactory依赖 Spring 对 Struts Action 的直接注入能力。升级会触发NoSuchMethodError: org.springframework.context.ConfigurableApplicationContext.getBeanFactory()类型错误。2.2 模块结构解析从web.xml到applicationContext.xml的控制流整个系统启动流程始于web.xml的三大核心配置!-- web.xml 片段 -- filter filter-namestruts2/filter-name filter-classorg.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter/filter-class /filter filter-mapping filter-namestruts2/filter-name url-pattern/*/url-pattern /filter-mapping context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener这里隐含两个容器生命周期ContextLoaderListener启动根 ApplicationContext加载 Service、DAO、DataSourceStrutsPrepareAndExecuteFilter启动子 ApplicationContext加载 Action。二者通过WebApplicationContextUtils.getWebApplicationContext(servletContext)关联。applicationContext.xml中的关键配置如下!-- applicationContext.xml 片段 -- bean iddataSource classcom.mchange.v2.c3p0.ComboPooledDataSource destroy-methodclose property namedriverClass valuecom.mysql.jdbc.Driver/ property namejdbcUrl valuejdbc:mysql://localhost:3306/hazard?useUnicodetrueamp;characterEncodingutf8/ property nameuser valueroot/ property namepassword value123456/ property nameinitialPoolSize value5/ property namemaxPoolSize value20/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameconfigLocation valueclasspath:mybatis-config.xml/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.hazard.dao/ property namesqlSessionFactoryBeanName valuesqlSessionFactory/ /bean注意MapperScannerConfigurer的basePackage指向com.hazard.dao但实际源码中该包下只有接口如HazardDao.java没有实现类——这是 MyBatis 动态代理的核心机制运行时生成HazardDaoImpl并注入SqlSessionTemplate。mybatis-config.xml中启用了二级缓存settings setting namecacheEnabled valuetrue/ setting namelazyLoadingEnabled valuetrue/ setting nameaggressiveLazyLoading valuefalse/ /settings这意味着HazardDao.selectById(1)第一次执行会查库并写入PerpetualCache第二次调用同一 ID 会命中缓存——但需注意MyBatis 二级缓存默认以 namespace 为单位且要求实体类实现Serializable。查看com.hazard.entity.HazardInfo.java其implements Serializable且serialVersionUID 1L符合要求。2.3 Struts2 拦截器链与权限控制实现细节权限控制未使用 Shiro 或 Spring Security而是基于 Struts2 自定义拦截器AuthInterceptorpublic class AuthInterceptor extends AbstractInterceptor { Override public String intercept(ActionInvocation invocation) throws Exception { HttpServletRequest request ServletActionContext.getRequest(); HttpSession session request.getSession(false); if (session null || session.getAttribute(user) null) { return login; // 跳转到 login.action } return invocation.invoke(); // 放行 } }该拦截器在struts.xml中注册package namedefault extendsstruts-default interceptors interceptor nameauth classcom.hazard.interceptor.AuthInterceptor/ interceptor-stack nameauthStack interceptor-ref namedefaultStack/ interceptor-ref nameauth/ /interceptor-stack /interceptors action namehazard_* classcom.hazard.action.HazardAction method{1} interceptor-ref nameauthStack/ result namesuccess/pages/hazard/list.jsp/result result nameinput/pages/hazard/add.jsp/result /action /package关键点在于{1}占位符将hazard_list映射到HazardAction.list()方法hazard_save映射到save()方法。这种命名约定避免了为每个 Action 单独配置action标签但要求方法名必须与 URL 后缀严格一致。HazardAction中的list()方法调用 Service 层时传入的是PageHelper.startPage(pageNum, pageSize)封装的Page对象而非原始参数——这是 MyBatis 分页插件pagehelper的典型用法源码中pom.xml引入了pagehelper-4.1.6.jar。3. 数据库设计与 MyBatis 实战从hazard_info表到动态 SQL 生成3.1 MySQL 5.7 下的表结构约束与索引策略hazard_info表定义如下摘自hazard.sqlCREATE TABLE hazard_info ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, hazard_code varchar(50) NOT NULL COMMENT 隐患编码, hazard_name varchar(200) NOT NULL COMMENT 隐患名称, level tinyint(4) NOT NULL DEFAULT 1 COMMENT 隐患等级1-一般2-较大3-重大, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0-待整改1-整改中2-已整改3-已关闭, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, creator_id bigint(20) NOT NULL COMMENT 创建人ID, PRIMARY KEY (id), UNIQUE KEY uk_hazard_code (hazard_code), KEY idx_status_level (status,level), KEY idx_creator_time (creator_id,create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT隐患信息表;注意三点ENGINEInnoDB是必须项否则 MyBatis 的Transactional注解无法生效MyISAM 不支持事务utf8mb4字符集支持 emoji 和四字节 UTF-8 字符避免Incorrect string value错误复合索引idx_status_level覆盖了常见查询条件WHERE status ? AND level ?而idx_creator_time支持按创建人时间排序分页。提示若本地 MySQL 5.7 未启用innodb_file_per_tableON导入后表空间文件ibdata1可能暴涨。建议在my.cnf中添加[mysqld] innodb_file_per_table13.2 MyBatis Mapper XML 中的动态 SQL 与缓存穿透防护HazardDao.xml中的selectListByCondition方法展示了典型的动态 SQLselect idselectListByCondition resultTypecom.hazard.entity.HazardInfo SELECT * FROM hazard_info WHERE 11 if testhazardCode ! null and hazardCode ! AND hazard_code LIKE CONCAT(%, #{hazardCode}, %) /if if teststatus ! null AND status #{status} /if if testlevel ! null AND level #{level} /if ORDER BY create_time DESC /select此处#{}使用预编译参数防止 SQL 注入而CONCAT(%, #{hazardCode}, %)实现模糊搜索。但需警惕当hazardCode为空字符串时LIKE %%会全表扫描。实际代码中HazardAction.list()方法做了前置校验public String list() { if (StringUtils.isNotBlank(hazardCode)) { hazardCode hazardCode.trim(); } else { hazardCode null; // 强制设为 null跳过 if 判断 } // ... 其他参数处理 page hazardService.selectListByCondition(hazardCode, status, level, pageNum, pageSize); return SUCCESS; }这就是业务层对 MyBatis 动态 SQL 的兜底控制。关于缓存HazardDao.xml显式启用了二级缓存mapper namespacecom.hazard.dao.HazardDao cache evictionLRU flushInterval60000 size1024 readOnlytrue/ select idselectById resultTypecom.hazard.entity.HazardInfo useCachetrue SELECT * FROM hazard_info WHERE id #{id} /select /mapperevictionLRU表示淘汰策略为最近最少使用flushInterval60000每 60 秒清空一次缓存size1024限制最大缓存条目数。useCachetrue是默认值可省略但显式写出便于审计。3.3 批量插入与事务边界insertBatch的正确打开方式隐患系统支持 Excel 导入批量新增对应HazardDao.insertBatch方法insert idinsertBatch parameterTypejava.util.List INSERT INTO hazard_info (hazard_code, hazard_name, level, status, creator_id, create_time) VALUES foreach collectionlist itemitem separator, (#{item.hazardCode}, #{item.hazardName}, #{item.level}, #{item.status}, #{item.creatorId}, NOW()) /foreach /insert该 SQL 依赖 MySQL 的多值INSERT语法5.7 原生支持。但关键在于事务控制HazardService.insertBatch(ListHazardInfo)方法上标注了TransactionalTransactional(rollbackFor Exception.class) public void insertBatch(ListHazardInfo list) { if (CollectionUtils.isEmpty(list)) return; // 每 1000 条分批提交防 OOM for (int i 0; i list.size(); i 1000) { int end Math.min(i 1000, list.size()); hazardDao.insertBatch(list.subList(i, end)); } }这里Transactional保证整个for循环在同一个事务中若第 5 批失败前 4 批自动回滚。rollbackFor Exception.class显式声明所有异常均触发回滚Spring 默认只对RuntimeException回滚。4. 运行部署与常见问题排错从 Tomcat 7 到 MySQL 连接池验证4.1 环境搭建三步走JDK、Tomcat、MySQL 版本匹配表组件推荐版本说明JDK1.7.0_80Spring 3.2 最低要求 JDK 1.6但 Struts 2.3.32 需要 JDK 1.7且java.timeAPI 尚未引入避免日期处理兼容性问题Tomcat7.0.94Servlet 3.0 容器完美兼容 Struts2.3 的web.xmlschemaTomcat 8 默认启用异步 Servlet可能干扰 Struts2 拦截器链MySQL5.7.21ONLY_FULL_GROUP_BY模式默认开启需在my.cnf中配置sql_modeSTRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION安装完成后验证 MySQL 连接池是否生效# 进入 MySQL 命令行 mysql -uroot -p123456 SHOW VARIABLES LIKE max_connections; -- 查看最大连接数 SHOW PROCESSLIST; -- 查看当前活跃连接应看到 c3p0 创建的连接若PROCESSLIST中无连接检查applicationContext.xml中ComboPooledDataSource的jdbcUrl是否包含?useUnicodetruecharacterEncodingutf8缺少此参数会导致中文乱码并可能触发连接拒绝。4.2 启动报错定位从ClassNotFoundException到NoSuchBeanDefinitionException常见启动失败场景及解决方案报错信息根本原因解决方案java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderListenerspring-web-3.2.18.RELEASE.jar未放入WEB-INF/lib检查lib目录是否存在该 JAR确认MANIFEST.MF中Class-Path未覆盖其路径Unable to instantiate Action, com.hazard.action.HazardActionHazardAction构造函数抛异常或 Spring 未成功注入依赖在HazardAction构造函数中加System.out.println(Action created);确认是否执行检查applicationContext.xml中HazardServiceBean 是否定义org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named hazardServiceHazardService接口未被 Spring 扫描到确认applicationContext.xml中context:component-scan base-packagecom.hazard.service/存在且HazardServiceImpl类上有Service(hazardService)注解特别注意Struts2 Action 的属性注入依赖 Spring 的ObjectFactory因此struts.xml中必须配置constant namestruts.objectFactory valuespring/否则即使applicationContext.xml定义了 Service BeanAction 中的Autowired也会为null。4.3 MyBatis 日志输出调试让 SQL 真实可见默认情况下 MyBatis 不打印 SQL需在log4j.properties中启用# log4j.properties log4j.logger.org.mybatisDEBUG log4j.logger.java.sql.ConnectionDEBUG log4j.logger.java.sql.StatementDEBUG log4j.logger.java.sql.PreparedStatementDEBUG log4j.logger.java.sql.ResultSetDEBUG重启 Tomcat 后控制台将输出类似日志DEBUG [http-bio-8080-exec-1] org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession DEBUG [http-bio-8080-exec-1] org.mybatis.spring.SqlSessionTemplate - JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection...] will not be managed by Spring DEBUG [http-bio-8080-exec-1] com.hazard.dao.HazardDao.selectListByCondition - Preparing: SELECT * FROM hazard_info WHERE 11 AND status ? ORDER BY create_time DESC DEBUG [http-bio-8080-exec-1] com.hazard.dao.HazardDao.selectListByCondition - Parameters: 0(Integer) DEBUG [http-bio-8080-exec-1] com.hazard.dao.HazardDao.selectListByCondition - Total: 12若看到 Parameters: null说明传入参数为null需检查 Action 中status属性是否被正确赋值Struts2 默认使用setter注入确保setStatus(Integer status)方法存在且可访问。5. 进阶技巧用 MyBatis Log Plugin 快速定位 SQL 性能瓶颈5.1 在 IntelliJ IDEA 中安装 MyBatis Log Plugin虽然系统本身未集成 MyBatis Log Plugin但开发调试时强烈推荐安装该插件JetBrains 插件市场搜 “MyBatis Log Plugin”。安装后重启 IDEA在Run→Edit Configurations→Environment variables中添加MYBATIS_LOG_PLUGIN_ENABLEDtrue然后启动 Tomcat Debug 模式插件会在Run窗口底部自动捕获所有 MyBatis SQL并高亮显示执行时间、参数值和结果行数[0.123ms] SELECT * FROM hazard_info WHERE status ? AND level ? Parameters: [0, 1] Result: 42 rows注意该插件仅在 IDE 内生效不影响生产环境。它通过字节码增强技术拦截PreparedStatement.execute*()方法无需修改任何代码。5.2 基于慢查询日志分析高频低效 SQLMySQL 5.7 默认关闭慢查询日志需手动启用SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; -- 超过 1 秒记为慢查询 SET GLOBAL log_output TABLE; -- 日志写入 mysql.slow_log 表然后模拟隐患列表页全量查询curl http://localhost:8080/hazard_list.action?status0hazardCode查询慢日志SELECT start_time, query_time, sql_text FROM mysql.slow_log WHERE sql_text LIKE %hazard_info% ORDER BY query_time DESC LIMIT 5;若发现SELECT * FROM hazard_info WHERE 11无条件全表扫描说明前端未传递有效查询参数需检查HazardAction.list()中hazardCode、status的判空逻辑是否被绕过。5.3 MyBatis 二级缓存失效场景验证表场景是否触发缓存失效说明同一namespace下执行INSERT/UPDATE/DELETE语句✅ 是MyBatis 自动清空该 namespace 下所有缓存HazardDao.updateStatus(Long id, Integer status)执行后再次selectById(id)✅ 是因updateStatus与selectById属同一 namespace缓存被清空HazardDao.selectById(1)与UserDao.selectById(1)同时执行❌ 否不同 namespace缓存隔离HazardDao.selectListByCondition(null, 0, null, 1, 10)与selectListByCondition(, 0, null, 1, 10)✅ 是参数null与序列化后不同视为不同 key验证方法在HazardDao.xml中cache/标签内添加blockingtrue然后在selectById方法前后加System.out.println(Cache hit: cache.get(key));观察控制台输出即可确认缓存命中率。实际开发中若发现selectListByCondition查询频繁但结果变化不大可考虑在 Service 层增加 Guava Cache 作为一级缓存减少 MyBatis 二级缓存的序列化开销——但这需要权衡内存占用与数据实时性。本文还有配套的精品资源点击获取