简介:一份面向Java Web开发者、详解Struts2+Spring+Hibernate三大框架整合的实操资源。内容涵盖SSH2分层整合思路、MyEclipse环境下的配置与调试,并附有可直接运行的完整示例代码。压缩包共54个文件,以jar依赖包、xml配置文件为主,同时包含java源码、class编译文件、jsp页面、properties配置及doc说明文档,整体约14.71MB,结构清晰便于对照学习。已有622人学习下载。源码中Action、Service、DAO各层实现齐全,配合applicationContext.xml、struts.xml、hibernate.cfg.xml等核心配置,可直观理解HTTP请求如何流转、业务逻辑如何调用、数据库映射如何完成;doc文档则对整合过程做了进一步梳理。学习者既能借此完成环境搭建,也可深入分析框架间的协作细节,适合正在学习SSH2整合或准备相关项目开发的人群。 读到 SSH2 这个组合名,Java Web 老开发应该都不陌生。它就是 Struts2 + Spring + Hibernate 三大框架的整合方案,在 Spring Boot 还没普及的年代,这套组合几乎就是国内企业级 Web 项目的标准答案,银行、教育、医疗、政企类系统的存量代码里,到今天仍有大量业务跑在它上面。我最近接手一个遗留系统改造,重新把三者串了一遍,发现很多细节看着简单,实际排序错了、类路径错了、作用域忘了配都会直接起不来服务。
这篇博文从整合思路、配置链路、分层源码、避坑心得四个角度展开,源码部分给了可以直接落地的骨架工程,适合正在维护老系统的开发、刚学完三个框架想串起来的学生,以及需要在本地快速搭一套可运行 Java Web 环境的新人。如果你想快速跑通,直接按第三步的配置抄即可。
1. 项目概述与整合思路
1.1 三大框架各管哪一段
SSH2 之所以能把三个独立的框架拼成一个完整应用,核心原因是职责划分得很清楚。
Struts2 是前台调度员,负责接收 HTTP 请求、解析参数、调用业务逻辑、跳转页面。它靠 struts.xml 里的 action 映射来决定一个 URL 该进哪个类哪个方法。
Spring 是容器管家,所有业务对象都放进它的 IoC 容器里管理,对象与对象之间通过依赖注入关联起来。事务也主要由 Spring 的声明式事务来控制,一个 @Transactional 就能让 Service 下的多个 DAO 操作要么全成功、要么全回滚。
Hibernate 是对象和数据库之间的翻译官,负责把 Java 对象映射成数据库表记录,也就是 ORM。它自带缓存、懒加载、级联策略,写数据层时可以少写大量 JDBC 代码。
把三者拆开看,每个框架单独用都不难,难的是整合后谁该创建谁、谁该管理谁、谁该在什么时机销毁。很多人把三个框架的 jar 包塞进工程后,Action 里直接new Service(),Service 里又new DAO(),表面上能用,实际上 Spring 完全接管不到对象生命周期,事务失效、Session 关闭异常、内存泄漏都是这么来的。
1.2 版本与工程的合理搭配
SSH2 最讲究版本匹配。我这次使用的组合是:Spring 4.3.30.RELEASE、Hibernate 5.2.18.Final、Struts 2.5.30,配合 MySQL 5.7 和 mysql-connector-java 5.1.49,JDK 1.8。这套搭配很成熟,网上资料也最多,遇到兼容性问题容易查到答案。
不建议用太新的 Spring 5.x + Hibernate 5.4 来搭 SSH2。Spring 5 的升级导致部分旧 API 被移除,Struts2 的 spring 插件需要同步升级,Hibernate 5.4 对 Session 生命周期和事务策略的改动也更激进,实际调试成本会高出不少。如果只是学习和维护老项目,没必要自我折腾。
工程骨架建议用 Maven 管理依赖,把 struts2-core、struts2-spring-plugin、spring-context、spring-orm、hibernate-core、mysql-connector-java 这几个核心依赖加上,剩下的交给 Maven 传递依赖解析。注意 struts2-spring-plugin 必须引入,否则 Struts2 无法识别 Spring 容器里创建的 Action。
2. 整合的核心配置逐段拆解
2.1 web.xml 是整条启动链路的入口
web.xml 决定整个 Web 应用启动时先做什么、后做什么。SSH2 整合后,web.xml 需要承担三件事:加载 Spring 的 applicationContext.xml、注册 Struts2 的前端控制器 Filter、加上请求编码过滤器。
配置顺序有讲究。ContextLoaderListener 要最先让 Tomcat 在启动阶段完成 Spring 容器的初始化,后续 Struts2 在处理请求时才能从容器里拿到业务 Bean。Struts2 在 2.5 版本的过滤器类路径是org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter,旧版本则是在dispatcher.ng.filter包下,写错就是 ClassNotFoundException,这一条最容易踩。
<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> <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> <filter> <filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class> </filter> <filter-mapping> <filter-name>struts2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>编码过滤器我习惯放在 Struts2 之前。如果反了,Struts2 先拿到未解码的参数,后面的 CharacterEncodingFilter 已经处理不了已经进入框架的参数内容,中文乱码问题会非常顽固。
2.2 applicationContext.xml 中如何注册 SessionFactory
Spring 接管 Hibernate 的关键,是在 applicationContext.xml 里注册一个LocalSessionFactoryBean。这个 Bean 会把数据源、实体扫描路径、Hibernate 的各种属性统一收编,之后真正使用 Session 的地方只需要注入 SessionFactory 就行。
我习惯把数据源、SessionFactory、事务管理器都写在一个配置文件里,分包 Scan 实体类。packagesToScan指的是实体类所在的包路径,Spring 会自动去扫描带 @Entity 注解的类,省掉老式 hibernate.cfg.xml 里一条条 mapping 的麻烦。
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/ssh2_demo?useUnicode=true&characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="packagesToScan" value="com.demo.po"/> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop> <prop key="hibernate.show_sql">true</prop> <prop key="hibernate.format_sql">true</prop> <prop key="hibernate.hbm2ddl.auto">update</prop> <prop key="hibernate.current_session_context_class">org.springframework.orm.hibernate5.SpringSessionContext</prop> </props> </property> </bean> <bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory"/> </bean>注意hibernate.current_session_context_class这一项,指的是当前线程上下文里的 Session 由谁管理。在集成 Spring 时,要指定为SpringSessionContext,Spring 才能把当前线程的 Session 与事务绑定在一起。如果不配,运行时会经常出现getCurrentSession抛异常的问题。
hbm2ddl.auto在学习和测试阶段可以配成 update,让 Hibernate 自动建表改表,但生产环境建议换成 validate 或直接关闭,避免框架在启动时对正式表结构做不可控调整。
2.3 struts.xml 与 Struts2 的 Spring 插件
Struts2 整合 Spring 后,struts.xml 里 action 的 class 不再写类的全限定名,而是写 Spring 容器中对应 Bean 的名字。为什么?因为 Spring 接管了 action 对象的创建权,Struts2 一旦收到请求,会通过 struts2-spring-plugin 去容器里按 id 查找 Bean。
<struts> <constant name="struts.objectFactory" value="spring"/> <package name="default" extends="struts-default"> <action name="user_list" class="userAction" method="list"> <result name="success">/list.jsp</result> </action> </package> </struts>这里的 class 是userAction,对应 Spring 容器中 id 为 userAction 的 Bean,而不是com.demo.action.UserAction这个类。如果写的是类名,请求进来时 Struts2 直接 new 一个 Action 对象,完全绕过 Spring,Action 里注入的 Service 字段就全是 null,报错极诡异。
顺便提一句,struts.objectFactory其实会被 struts2-spring-plugin 自动改成 spring,但我会显式写出来,让别人看配置时一眼能看出整合点。
3. 源码结构与各层实现要点
3.1 实体与 DAO 层的两种写法
先看实体类,最简单的做法是加 JPA 注解,一行 @Entity 加一行 @Table 就能让 Hibernate 识别。字段直接用 @Column 映射即可,没必要每个字段都加复杂约束。
写 DAO 层时有两条路线:继承HibernateDaoSupport配合 HibernateTemplate,或者直接注入 SessionFactory。早期 SSH 项目大量使用前者,但 Spring 3.0 之后官方逐渐不再推荐 HibernateTemplate,Hibernate 5.x 后更是明显边缘化。我建议直接注入 SessionFactory,代码直观,debug 时也容易定位问题。
@Repository public class UserDao { @Autowired private SessionFactory sessionFactory; @SuppressWarnings("unchecked") public List<User> list() { return sessionFactory.getCurrentSession() .createCriteria(User.class) .list(); } public void save(User user) { sessionFactory.getCurrentSession().saveOrUpdate(user); } }@Repository能让 Spring 自动扫描并创建 DAO 实例,同时把持久层异常转换成 Spring 的DataAccessException。为什么用getCurrentSession而不是openSession?getCurrentSession 拿到的 Session 会被绑定到当前事务上,由 Spring 统一管理生命周期;openSession 得自己手动关闭,一旦在某些分支忘记 close,连接就泄漏了。
3.2 Service 层事务边界的设定
Service 层是事务大事记里最该放 @Transactional 的地方。DAO 层面通常只处理单表读写,如果事务放在 DAO 上,一次业务操作要调用多个 DAO 时,每条 DAO 方法会各自开一个事务,中间出了错根本回滚不了。正确做法是把事务边界放在 Service 方法上,让一个业务操作整体成为一个事务。
@Service @Transactional public class UserService { @Autowired private UserDao userDao; public List<User> list() { return userDao.list(); } public void save(User user) { userDao.save(user); } }如果类上标了 @Transactional,类里所有方法都会进入事务,默认不读操作也是只读事务。需要精确控制时可以移到方法上,比如查询方法配置成@Transactional(readOnly = true),写入方法再用默认配置。这样做的目的不只是性能,readOnly 提示对底层数据库优化器友好,也能防止误操作把查询日志变成写入日志。
事务管理器的注册方式在 2.2 节已经写了,那句<tx:annotation-driven>是开关,没有它 @Transactional 就是普通注释,不会产生任何效果。
3.3 Action 类的 Spring 化与作用域选择
Action 类接入 Spring 时有一个极易踩的坑:作用域。Struts2 默认每次请求都会创建一个新的 Action 实例,保证并发安全。Spring 管理的 Bean 默认是单例,如果你不特意设置 Action 的 scope,所有用户请求共用同一个 Action 对象,Action 里的成员变量会被并发污染。最典型的症状就是:A 用户查列表,页面刷新后看到的是 B 用户的数据。
解决办法是给 Action 标上@Scope("prototype"),每次请求都从 Spring 容器取一个新实例。
@Controller @Scope("prototype") public class UserAction extends ActionSupport { @Autowired private UserService userService; private List<User> userList; public String list() { userList = userService.list(); return SUCCESS; } public List<User> getUserList() { return userList; } public void setUserList(List<User> userList) { this.userList = userList; } }Service 和 DAO 保持单例没问题,它们内部没有请求级别的状态信息,SessionFactory、DataSource 这类资源线程安全。Action 必须 prototype,这也是 SSH2 老项目里“Action 由 Spring 管理”看上去简单、实际上坑最深的地方。
struts.xml 里 action 的 class 和 Spring 的 bean 名要保持一致。我习惯统一用小写驼峰,比如 UserAction 类的 bean 名 userAction,struts.xml 里就写 userAction,避免花时间排查名称对不上的问题。
4. 常见报错与实战避坑
4.1 启动期典型报错速查
我把实际整合过程中最容易出现的启动期报错整理成一张速查表,排查时直接对号入座。
| 报错信息 | 大概率原因 | 解决方式 |
|---|---|---|
| ClassNotFoundException: dispatcher.ng.filter.StrutsPrepareAndExecuteFilter | Struts2 版本与过滤器类路径不匹配 | 2.5 以后用 dispatcher.filter.StrutsPrepareAndExecuteFilter |
| BeanCreationException: sessionFactory 创建失败 | 数据库连不上或实体包路径扫描不到 | 检查 jdbc 连接参数、packagesToScan 路径 |
| NoSuchBeanDefinitionException: userAction | struts.xml 中 action 的 class 名没有对应 Spring Bean | 确认 @Controller、@Scope 注解、bean 名称 |
| plugin not found 或 Unable to load configuration | struts2-spring-plugin 缺失 | pom.xml 中补依赖 |
| jar 包冲突导致 ClassCastException | javassist、antlr、slf4j 多版本互相打架 | 统一版本并用 Maven 排除传递依赖 |
启动问题大多数集中在“配置顺序”和“类路径”上。看到类加载相关错误时,先检查是不是 jar 包被重复引入,特别是 slf4j 和 javassist,不同框架可能依赖不同版本,Maven 依赖树里常能看到两份。
4.2 运行期容易踩的坑
启动没问题不代表整合就成功了,运行期最经典的坑是 LazyInitializationException。这个报错发生在页面渲染时:Hibernate 默认懒加载关联对象,Service 层事务结束后 Session 关闭,JSP 在页面上访问关联属性时 Session 已经不在了,框架直接抛异常。
解决思路有三条:一是把关联对象的查询改成join fetch,一次性把需要的数据拉回来;二是在 web.xml 里配 OpenSessionInViewFilter,把 Session 的生命周期延长到视图渲染结束;三是干脆不用懒加载,在一对一、多对一这种关联上用 EAGER 加载。我个人的习惯是优先用 join fetch,毕竟 OpenSessionInViewFilter 会拉长数据库连接占用时间,并发高时会明显拖垮数据库连接池。
中文乱码又是一个高频坑。Struts2 默认只按application/x-www-form-urlencoded解析 POST 参数,如果你的页面本身就是 UTF-8,一定要保证前端页面编码、web.xml 的 CharacterEncodingFilter、数据库连接 URL 里的 characterEncoding 三处完全一致。数据库连接 URL 少写了useUnicode=true&characterEncoding=utf8,即使前端和过滤器都对,存进库里的中文照样乱。
还有一个容易被忽略的是 Hibernate 缓存导致的“改库没生效”。启动时配了 hbm2ddl.auto=update,字段类型变了但缓存没刷新,部分 SQL 会用旧元数据执行。这个在测试环境不常遇到,但如果改过实体类后复现出一些莫名其妙的 SQL 错误,先清理数据库里的表和 Hibernate 的二级缓存。
4.3 老框架使用中的取舍
我不建议在接手老系统后立刻把 SSH2 整体替换成 Spring Boot + MyBatis,改动面太大,业务验证和回归测试的成本比想象中高得多。比较稳妥的做法是先把 SSH2 的整合链路理清楚,让项目能稳定编译、正常启动、事务生效,再以增量方式逐步替换。
如果真要改造,优先级我建议是:先换连接池,把原来的 DriverManagerDataSource 换成 Druid 或 HikariCP,连接管理更规范,排查 SQL 也能拿到监控数据;再换 DAO 层,从 Hibernate 迁到 MyBatis,因为 DAO 层通常是最容易单元测试的边界;最后才是把 Struts2 换成 Spring MVC,Action 层涉及大量页面跳转和参数封装,影响面最大,放最后动。
整个替换过程要保持各个框架的接口边界清晰,才能做到改一层不牵动另外两层。如果没想清楚这一步,很容易改到一半连二次开发都做不了。
5. 源码说明与扩展建议
5.1 源码包里有什么
这次配套提供的源码是一个 Maven 工程,目录结构如下:
ssh2-demo ├── pom.xml ├── src/main/java │ ├── com/demo/action/UserAction.java │ ├── com/demo/dao/UserDao.java │ ├── com/demo/service/UserService.java │ └── com/demo/po/User.java ├── src/main/resources │ ├── applicationContext.xml │ ├── struts.xml │ └── jdbc.properties └── src/main/webapp ├── WEB-INF/web.xml └── list.jsp跑起来的前提是你机器上有 JDK 8、Maven 3.5 以上、MySQL 5.7。把 jdbc.properties 里的用户名密码改成自己本机的,启动 Tomcat 7 或 8,访问http://localhost:8080/ssh2-demo/user_list.action就能看到一个简单的用户列表页面。数据表不用手动建,Hibernate 的 hbm2ddl.auto 会自动帮你创建。
由于博客不方便直接挂附件,源码我打包放在网盘链接里,拿到后直接mvn clean tomcat7:run即可启动。如果本地环境实在起不来,优先检查 pom.xml 里 spring、struts、hibernate 三个版本是否与代码一致。
5.2 后续还能怎么改
这个骨架工程虽然小,但已经覆盖了 SSH2 整合的完整链路。如果想把骨架变成真正的业务系统,有几个值得动手的方向:把单表 User 的业务扩成多表关联,测试 Hibernate 的懒加载和缓存策略;把用户列表改为分页查询,在 DAO 层封装一个通用的分页工具方法;给 Service 层加一层接口,把事务边界和实现分离,方便后续替换实现类。
过去我接手完 SSH2 项目后的习惯是,先在这个小骨架里把各种异常场景都试一遍,比如故意把事务回滚、故意访问不存在的 action 映射,看看整体框架的响应行为和日志输出。这样真正进老系统排查问题时,心里对每一步链路都很有底。
最后再分享一个我实测下来很管用的技巧:给 SSH2 项目配置一个统一的启动日志级别,把 log4j 的 rootLogger 调成 DEBUG,然后逐步降到 INFO。调试整合期问题时要看 Spring 容器加载了哪些 Bean、Hibernate 执行的每一条 SQL,等系统稳定后再把日志调回正常级别,少走很多弯路。
本文还有配套的精品资源,点击获取