SSH经典架构实战解析:Spring3.2+Struts2.3+MyBatis遗留系统拆解指南
2026/9/15 13:03:44 网站建设 项目流程

简介:这是一套基于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.2?Struts2.3?这不都是十年前的老古董了吗?确实,主流项目早已迁移到 Spring Boot 3.x 和 MyBatis-Plus,但正因如此,这套「隐患排查系统」反而成了极少数能完整呈现经典 SSH(Spring + 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_mode=STRICT_TRANS_TABLES的关键版本,直接影响INSERT IGNOREON 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.xmlapplicationContext.xml的控制流

整个系统启动流程始于web.xml的三大核心配置:

<!-- web.xml 片段 --> <filter> <filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class> </filter> <filter-mapping> <filter-name>struts2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <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>

这里隐含两个容器生命周期:ContextLoaderListener启动根 ApplicationContext(加载 Service、DAO、DataSource),StrutsPrepareAndExecuteFilter启动子 ApplicationContext(加载 Action)。二者通过WebApplicationContextUtils.getWebApplicationContext(servletContext)关联。applicationContext.xml中的关键配置如下:

<!-- applicationContext.xml 片段 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> <property name="driverClass" value="com.mysql.jdbc.Driver"/> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/hazard?useUnicode=true&amp;characterEncoding=utf8"/> <property name="user" value="root"/> <property name="password" value="123456"/> <property name="initialPoolSize" value="5"/> <property name="maxPoolSize" value="20"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.hazard.dao"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean>

注意MapperScannerConfigurerbasePackage指向com.hazard.dao,但实际源码中该包下只有接口(如HazardDao.java),没有实现类——这是 MyBatis 动态代理的核心机制:运行时生成HazardDaoImpl并注入SqlSessionTemplatemybatis-config.xml中启用了二级缓存:

<settings> <setting name="cacheEnabled" value="true"/> <setting name="lazyLoadingEnabled" value="true"/> <setting name="aggressiveLazyLoading" value="false"/> </settings>

这意味着HazardDao.selectById(1)第一次执行会查库并写入PerpetualCache,第二次调用同一 ID 会命中缓存——但需注意:MyBatis 二级缓存默认以 namespace 为单位,且要求实体类实现Serializable。查看com.hazard.entity.HazardInfo.java,其implements SerializableserialVersionUID = 1L,符合要求。

2.3 Struts2 拦截器链与权限控制实现细节

权限控制未使用 Shiro 或 Spring Security,而是基于 Struts2 自定义拦截器AuthInterceptor

public 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 name="default" extends="struts-default"> <interceptors> <interceptor name="auth" class="com.hazard.interceptor.AuthInterceptor"/> <interceptor-stack name="authStack"> <interceptor-ref name="defaultStack"/> <interceptor-ref name="auth"/> </interceptor-stack> </interceptors> <action name="hazard_*" class="com.hazard.action.HazardAction" method="{1}"> <interceptor-ref name="authStack"/> <result name="success">/pages/hazard/list.jsp</result> <result name="input">/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.sql):

CREATE 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='隐患信息表';

注意三点:

  1. ENGINE=InnoDB是必须项,否则 MyBatis 的@Transactional注解无法生效(MyISAM 不支持事务);
  2. utf8mb4字符集支持 emoji 和四字节 UTF-8 字符,避免Incorrect string value错误;
  3. 复合索引idx_status_level覆盖了常见查询条件WHERE status = ? AND level = ?,而idx_creator_time支持按创建人+时间排序分页。

提示:若本地 MySQL 5.7 未启用innodb_file_per_table=ON,导入后表空间文件ibdata1可能暴涨。建议在my.cnf中添加:

[mysqld] innodb_file_per_table=1

3.2 MyBatis Mapper XML 中的动态 SQL 与缓存穿透防护

HazardDao.xml中的selectListByCondition方法展示了典型的动态 SQL:

<select id="selectListByCondition" resultType="com.hazard.entity.HazardInfo"> SELECT * FROM hazard_info WHERE 1=1 <if test="hazardCode != null and hazardCode != ''"> AND hazard_code LIKE CONCAT('%', #{hazardCode}, '%') </if> <if test="status != null"> AND status = #{status} </if> <if test="level != 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 namespace="com.hazard.dao.HazardDao"> <cache eviction="LRU" flushInterval="60000" size="1024" readOnly="true"/> <select id="selectById" resultType="com.hazard.entity.HazardInfo" useCache="true"> SELECT * FROM hazard_info WHERE id = #{id} </select> </mapper>

eviction="LRU"表示淘汰策略为最近最少使用,flushInterval="60000"每 60 秒清空一次缓存,size="1024"限制最大缓存条目数。useCache="true"是默认值,可省略,但显式写出便于审计。

3.3 批量插入与事务边界:insertBatch的正确打开方式

隐患系统支持 Excel 导入批量新增,对应HazardDao.insertBatch方法:

<insert id="insertBatch" parameterType="java.util.List"> INSERT INTO hazard_info (hazard_code, hazard_name, level, status, creator_id, create_time) VALUES <foreach collection="list" item="item" separator=","> (#{item.hazardCode}, #{item.hazardName}, #{item.level}, #{item.status}, #{item.creatorId}, NOW()) </foreach> </insert>

该 SQL 依赖 MySQL 的多值INSERT语法(5.7+ 原生支持)。但关键在于事务控制:HazardService.insertBatch(List<HazardInfo>)方法上标注了@Transactional

@Transactional(rollbackFor = Exception.class) public void insertBatch(List<HazardInfo> 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.xmlschema,Tomcat 8+ 默认启用异步 Servlet,可能干扰 Struts2 拦截器链
MySQL5.7.21ONLY_FULL_GROUP_BY模式默认开启,需在my.cnf中配置sql_mode=STRICT_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.xmlComboPooledDataSourcejdbcUrl是否包含?useUnicode=true&characterEncoding=utf8,缺少此参数会导致中文乱码并可能触发连接拒绝。

4.2 启动报错定位:从ClassNotFoundExceptionNoSuchBeanDefinitionException

常见启动失败场景及解决方案:

报错信息根本原因解决方案
java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderListenerspring-web-3.2.18.RELEASE.jar未放入WEB-INF/lib检查lib目录是否存在该 JAR,确认MANIFEST.MFClass-Path未覆盖其路径
Unable to instantiate Action, com.hazard.action.HazardActionHazardAction构造函数抛异常,或 Spring 未成功注入依赖HazardAction构造函数中加System.out.println("Action created");,确认是否执行;检查applicationContext.xmlHazardServiceBean 是否定义
org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'hazardService'HazardService接口未被 Spring 扫描到确认applicationContext.xml<context:component-scan base-package="com.hazard.service"/>存在,且HazardServiceImpl类上有@Service("hazardService")注解

特别注意:Struts2 Action 的属性注入依赖 Spring 的ObjectFactory,因此struts.xml中必须配置:

<constant name="struts.objectFactory" value="spring"/>

否则即使applicationContext.xml定义了 Service Bean,Action 中的@Autowired也会为null

4.3 MyBatis 日志输出调试:让 SQL 真实可见

默认情况下 MyBatis 不打印 SQL,需在log4j.properties中启用:

# log4j.properties log4j.logger.org.mybatis=DEBUG log4j.logger.java.sql.Connection=DEBUG log4j.logger.java.sql.Statement=DEBUG log4j.logger.java.sql.PreparedStatement=DEBUG log4j.logger.java.sql.ResultSet=DEBUG

重启 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 1=1 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,在RunEdit ConfigurationsEnvironment variables中添加:

MYBATIS_LOG_PLUGIN_ENABLED=true

然后启动 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 基于慢查询日志分析高频低效 SQL

MySQL 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?status=0&hazardCode="

查询慢日志:

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 1=1无条件全表扫描,说明前端未传递有效查询参数,需检查HazardAction.list()hazardCodestatus的判空逻辑是否被绕过。

5.3 MyBatis 二级缓存失效场景验证表

场景是否触发缓存失效说明
同一namespace下执行INSERT/UPDATE/DELETE语句✅ 是MyBatis 自动清空该 namespace 下所有缓存
HazardDao.updateStatus(Long id, Integer status)执行后,再次selectById(id)✅ 是updateStatusselectById属同一 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/>标签内添加blocking="true",然后在selectById方法前后加System.out.println("Cache hit: " + cache.get(key));,观察控制台输出即可确认缓存命中率。

实际开发中,若发现selectListByCondition查询频繁但结果变化不大,可考虑在 Service 层增加 Guava Cache 作为一级缓存,减少 MyBatis 二级缓存的序列化开销——但这需要权衡内存占用与数据实时性。

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

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

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

立即咨询