news 2026/10/18 2:51:15

SSH2框架整合实战:Spring+Struts2+Hibernate事务协同方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SSH2框架整合实战:Spring+Struts2+Hibernate事务协同方案

简介:本资源是一份面向Java Web开发初学者与中级工程师的SSH2框架整合实战指南,聚焦Struts2、Spring、Hibernate三大主流框架在MyEclipse环境下的协同配置与工程落地。资源提供完整可运行的整合示例,覆盖Web层(Action+JSP)、业务层(Service)、持久层(DAO+Hibernate映射)全链路实现,并详解依赖注入、拦截器配置、事务管理及数据库连接等核心整合难点。压缩包共54个文件,含26个关键jar包(支撑框架运行)、9个XML配置文件(applicationContext.xml、struts.xml、hibernate.cfg.xml等)、5个Java源码文件(分层逻辑实现)、2个JSP视图页及1个详尽的Word文档说明,整体14.71MB,结构清晰、开箱即用。已有623人学习下载,读者可直接导入MyEclipse调试运行,通过源码逐层理解MVC分层协作机制、Spring中心调度作用及Hibernate ORM映射实践,是掌握Java企业级开发基础架构整合的高实用性入门范例。

1. SSH2三大框架整合不是“拼积木”,而是让Struts2、Spring、Hibernate在同一个事务上下文里呼吸

你有没有试过把Struts2的Action交给Spring管理,却在Service层调用Hibernate时发现session自动关闭?或者配置了Spring声明式事务,但save操作成功、update却没生效,日志里连SQL都没打出来?这不是你配错了XML,而是SSH2整合中事务传播、对象生命周期、代理链路三个黑匣子没对齐。这个项目不是教你怎么写hello world,而是提供一套经过真实业务压测验证的整合骨架:它用Spring 3.2.18 + Struts2 2.3.37 + Hibernate 4.3.11(全注解+XML混合配置),跑通了用户注册→订单生成→库存扣减的完整事务链,所有DAO都走Spring管理的SessionFactory,所有Service方法都受@Transactional控制,连Struts2的ModelDriven和Validation拦截器都和Spring AOP共存无冲突。适合正在维护老系统、需要升级框架但不敢动核心流程的后端开发者,也适合想搞懂“为什么网上教程总缺半截”的Java Web学习者——它不讲理论,只给你能mvn clean tomcat7:run直接看到控制台打印出TransactionSynchronizationManager: resources={org.hibernate.SessionFactory=SessionImpl}的源码包。


2. 环境与依赖:JDK、容器、Maven坐标一个都不能少的硬性清单

2.1 JDK与Web容器版本锁定:为什么必须是JDK 7 + Tomcat 7

这个项目明确要求JDK 7u80(非JDK 8+)和Tomcat 7.0.99(非Tomcat 8/9)。原因很实在:Struts2 2.3.x系列在JDK 8的lambda表达式解析上存在ClassFormatError,而Tomcat 7的Servlet 3.0规范与Spring 3.2的ContextLoaderListener兼容性经过千次重启验证。如果你强行用JDK 11,会在org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter初始化时抛出java.lang.UnsupportedClassVersionError;换成Tomcat 9则因web.xml中<listener>标签顺序问题导致Spring Context加载失败,ApplicationContext为空。我一般会建一个独立的jdk7-tomcat7目录,把JDK 7和Tomcat 7解压进去,再在IDEA里新建Project时强制指定JDK路径——别信“向下兼容”,这里没有后悔药。

# 验证JDK版本(必须输出1.7.0_80) $JAVA_HOME/bin/java -version # 验证Tomcat版本(必须输出Apache Tomcat/7.0.99) $CATALINA_HOME/bin/version.sh

提示:Windows用户注意JAVA_HOME路径不能含中文或空格,否则Tomcat启动时catalina.sh会解析失败,报错Cannot find $JAVA_HOME/bin/java。

2.2 Maven依赖树:三个框架的版本咬合点在哪

Mavenpom.xml里最关键的不是版本号本身,而是传递依赖的排除逻辑。比如Hibernate 4.3.11默认引入javassist 3.18.1-GA,但Struts2 2.3.37需要javassist 3.11.0-GA,不显式排除就会在ActionProxy创建时抛NoSuchMethodError。同理,Spring 3.2.18自带的commons-logging和Struts2依赖的log4j-over-slf4j存在桥接冲突。以下是必须保留的核心依赖块(其他如MySQL驱动、JUnit等按需添加):

<!-- Spring核心 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>3.2.18.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>3.2.18.RELEASE</version> </dependency> <!-- Hibernate 4.3.x --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>4.3.11.Final</version> <exclusions> <exclusion> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> </exclusion> </exclusions> </dependency> <!-- Struts2核心(注意:必须用struts2-spring-plugin) --> <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-core</artifactId> <version>2.3.37</version> </dependency> <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-spring-plugin</artifactId> <version>2.3.37</version> </dependency> <!-- 手动指定统一的javassist版本 --> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.11.0-GA</version> </dependency>

2.3 web.xml:三大框架入口的加载顺序就是生命线

web.xml里<listener>和<filter>的声明顺序不是格式要求,而是执行时序契约。必须严格按以下顺序:

  1. ContextLoaderListener(Spring容器启动)
  2. StrutsPrepareAndExecuteFilter(Struts2拦截器链注册)
  3. OpenSessionInViewFilter(可选,但本项目启用以解决LazyInitializationException)
<!-- 1. Spring容器监听器:必须第一个 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- 2. Struts2过滤器:必须第二个,且init-param指定struts.xml位置 --> <filter> <filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class> <init-param> <param-name>config</param-name> <param-value>struts-default.xml,struts-plugin.xml,WEB-INF/classes/struts.xml</param-value> </init-param> </filter> <filter-mapping> <filter-name>struts2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 3. OpenSessionInViewFilter:解决N+1查询,必须在Struts2之后 --> <filter> <filter-name>openSessionInViewFilter</filter-name> <filter-class>org.springframework.orm.hibernate4.support.OpenSessionInViewFilter</filter-class> </filter> <filter-mapping> <filter-name>openSessionInViewFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

注意:ContextLoaderListener加载的是applicationContext.xml(Spring根容器),而StrutsPrepareAndExecuteFilter加载的是struts.xml(Struts2动作映射)。如果把Struts2过滤器放前面,它会先尝试创建Action,但此时Spring容器还没起来,@Autowired的Service就是null——这是新手最常翻车的第一步。


3. 核心模块拆解:从UserAction到UserDao的完整调用链

3.1 Struts2 Action层:如何让Spring接管Action实例

传统Struts2的Action是每次请求new出来的,无法注入Spring Bean。本项目通过struts2-spring-plugin实现@Service注入,关键在两点:一是struts.xml中<package>必须继承struts-default并启用spring拦截器栈;二是Action类必须用@Scope("prototype")标注(因为Action是多例的,Spring默认单例)。

<!-- struts.xml --> <package name="default" extends="struts-default"> <!-- 启用spring拦截器栈 --> <interceptors> <interceptor-stack name="springDefaultStack"> <interceptor-ref name="defaultStack"/> </interceptor-stack> </interceptors> <default-interceptor-ref name="springDefaultStack"/> <action name="userRegister" class="com.example.action.UserAction" method="register"> <result name="success">/success.jsp</result> <result name="input">/register.jsp</result> </action> </package>
// UserAction.java @Component("userAction") // 必须有Bean名称,struts.xml中class属性要匹配 @Scope("prototype") // 关键!避免多线程共享状态 public class UserAction extends ActionSupport implements ModelDriven<User> { private User user = new User(); @Autowired private UserService userService; // Spring注入的Service public String register() { try { userService.saveUser(user); // 调用Spring管理的Service return SUCCESS; } catch (Exception e) { addActionError(e.getMessage()); return INPUT; } } @Override public User getModel() { return user; } }

逻辑说明:当访问/userRegister.action时,Struts2不再自己newUserAction,而是从Spring容器中getBean("userAction")。由于@Scope("prototype"),每次请求都获得新实例,既保证线程安全,又享受Spring的依赖注入和AOP增强。

3.2 Spring Service层:事务边界与异常处理的黄金法则

UserService是事务控制的核心。本项目采用@Transactional注解方式,但绝不是简单加在方法上就完事。关键配置在applicationContext.xml中:

<!-- applicationContext.xml --> <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/> <bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory"/> </bean>

proxy-target-class="true"表示强制使用CGLIB代理(而非JDK动态代理),这样才能代理没有接口的Service类。UserService实现如下:

@Service("userService") @Transactional // 类级别声明,整个类方法都受事务控制 public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void saveUser(User user) throws Exception { // 1. 先保存用户 userDao.save(user); // 2. 模拟可能失败的操作(如发邮件、调用外部API) if (user.getEmail().contains("error")) { throw new RuntimeException("Email validation failed"); } // 3. 如果上面抛异常,此处的update不会执行,且user.save回滚 user.setStatus("ACTIVE"); userDao.update(user); } }

参数说明:

  • propagation = Propagation.REQUIRED:如果当前存在事务,则加入该事务;否则新建事务(最常用)。
  • rollbackFor = Exception.class:任何Exception及其子类都触发回滚(默认只对RuntimeException回滚)。
  • 方法内userDao.save()和userDao.update()共享同一个Hibernate Session,因为它们在同一个事务上下文中。

3.3 Hibernate DAO层:手写HQL与命名查询的取舍

DAO层不使用Spring JdbcTemplate,而是纯Hibernate API。UserDaoImpl通过@Repository标注,并注入SessionFactory:

@Repository("userDao") public class UserDaoImpl implements UserDao { @Autowired private SessionFactory sessionFactory; @Override public void save(User user) { // 获取当前事务绑定的Session(非openSession()!) Session session = sessionFactory.getCurrentSession(); session.save(user); } @Override public User findById(Long id) { Session session = sessionFactory.getCurrentSession(); // 使用命名查询(推荐:SQL集中管理,便于优化) Query query = session.getNamedQuery("User.findById"); query.setParameter("id", id); return (User) query.uniqueResult(); } @Override public List<User> findByStatus(String status) { Session session = sessionFactory.getCurrentSession(); // 手写HQL(适合简单查询,开发快) String hql = "FROM User u WHERE u.status = :status"; Query query = session.createQuery(hql); query.setParameter("status", status); return query.list(); } }

提示:sessionFactory.getCurrentSession()返回的是与当前Spring事务绑定的Session,生命周期由Spring管理;而sessionFactory.openSession()返回的是独立Session,需手动close(),否则内存泄漏。本项目所有DAO方法都用getCurrentSession(),这是整合成功的标志。


4. 避坑指南:那些让开发者凌晨三点还在看日志的典型问题

4.1 现象:Action中@Autowired的Service为null

原因:struts.xml中<action>的class属性写的是全限定名(如com.example.action.UserAction),而不是Spring Bean名称(如userAction)。Struts2找不到对应Bean,就退化为自己new实例,自然无法注入。
解决:检查struts.xml,确保class值与@Component("xxx")中的名称一致,并确认struts2-spring-plugin已引入。

4.2 现象:save操作数据库有记录,但update不生效,且无SQL日志

原因:UserService类上加了@Transactional,但saveUser()方法内部调用了另一个未加事务的方法(如sendEmail()),导致事务传播中断。更隐蔽的是:UserDaoImpl中误用了sessionFactory.openSession(),其Session未绑定到Spring事务,update()操作在独立Session中执行,commit时被忽略。
解决:统一使用sessionFactory.getCurrentSession();检查所有Service方法是否都在事务代理链内(可通过System.out.println(this.getClass())确认是否为CGLIB代理类)。

4.3 现象:页面报LazyInitializationException: could not initialize proxy - no Session

原因:JSP中访问了User对象的延迟加载集合(如user.getOrders()),但此时HTTP请求已结束,Spring事务已提交,Hibernate Session已关闭。
解决:启用OpenSessionInViewFilter(已在web.xml中配置),或改用JOIN FETCH在DAO层一次性查出关联数据(推荐):

// 在UserDaoImpl中添加 @Override public User findByIdWithOrders(Long id) { Session session = sessionFactory.getCurrentSession(); String hql = "FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id"; Query query = session.createQuery(hql); query.setParameter("id", id); return (User) query.uniqueResult(); }

4.4 现象:Struts2表单校验失败,错误信息不显示在JSP

原因:struts.xml中<action>的<result>类型不是dispatcher(默认),而是redirect。redirect会丢失ActionContext中的fieldErrors。
解决:确保<result>未指定type属性(即默认dispatcher),或显式写成<result type="dispatcher">。同时JSP中必须用<s:fielderror/>标签输出错误。

4.5 现象:MySQL中文乱码,存入数据库全是问号

原因:JDBC URL未指定字符集,或数据库表字符集不是utf8mb4。
解决:修改applicationContext.xml中dataSource的URL:

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=GMT%2B8"/> <!-- 其他属性... --> </bean>

并在MySQL中执行:ALTER DATABASE test CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;


5. 运行与调试:从零启动到断点验证的全流程

5.1 Maven构建与Tomcat部署的三步法

第一步:清理并编译(确保无残留class)

mvn clean compile

第二步:复制资源到target(关键!src/main/resources下的applicationContext.xml等必须出现在target/classes/)

mvn resources:resources

第三步:启动嵌入式Tomcat(本项目已配置tomcat7-maven-plugin)

mvn tomcat7:run

注意:不要用mvn package再手动部署,tomcat7:run会自动热加载src/main/webapp和src/main/resources,改完XML或JSP按Ctrl+S就能刷新。

5.2 断点验证:确认三大框架真正“握手成功”

在IDEA中设置三个断点:

  1. UserAction.register()入口:确认userService不为null,且this.getClass()输出类似com.sun.proxy.$Proxy23(说明CGLIB代理生效);
  2. UserServiceImpl.saveUser()中userDao.save(user)前:查看Thread.currentThread().getStackTrace(),确认调用栈包含TransactionInterceptor.invoke();
  3. UserDaoImpl.save()中sessionFactory.getCurrentSession()后:观察session对象的hibernateSession字段,其transaction属性应为ManagedTransaction(Spring管理的事务)。

如果第2步没看到TransactionInterceptor,说明@Transactional未生效——检查applicationContext.xml中<tx:annotation-driven>是否漏写,或UserService类是否被<context:component-scan>扫描到。

5.3 日志开关:让Hibernate和Spring说出真相

在log4j.properties中开启关键日志:

# 显示SQL语句(含参数) log4j.logger.org.hibernate.SQL=DEBUG log4j.logger.org.hibernate.type.descriptor.sql.BasicBinder=TRACE # 显示Spring事务行为 log4j.logger.org.springframework.transaction=DEBUG log4j.logger.org.springframework.orm.hibernate4.HibernateTransactionManager=DEBUG # 显示Struts2拦截器链 log4j.logger.com.opensymphony.xwork2.interceptor=DEBUG

启动后,控制台会打印:

[DEBUG] org.springframework.transaction.interceptor.TransactionInterceptor - Getting transaction for [com.example.service.UserServiceImpl.saveUser] [DEBUG] org.hibernate.SQL - insert into user (name, email, status) values (?, ?, ?) [TRACE] org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [VARCHAR] - [张三]

看到这三行,说明事务、SQL、参数绑定全部就绪。


6. 二次开发实战:给现有项目接入Redis缓存的改造步骤

6.1 场景定位:为什么选Redis而不是Ehcache

本项目用户查询(findById)频次高、数据量中等(万级)、更新不频繁(每天<100次),符合Redis缓存特征。Ehcache虽省内存,但集群环境下同步复杂;而本项目未来要水平扩展,Redis天然支持主从+哨兵,改造成本最低。重点不是“上Redis”,而是让缓存穿透、雪崩、击穿问题在SSH2架构下可控。

6.2 代码改造四步走:从DAO到Controller的无缝接入

第一步:添加Redis依赖
在pom.xml中加入spring-data-redis和jedis(注意版本兼容Spring 3.2):

<dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>1.8.23.RELEASE</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.3</version> </dependency>

第二步:配置Redis连接工厂
在applicationContext-redis.xml中(独立配置文件,避免污染主配置):

<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> <property name="hostName" value="127.0.0.1"/> <property name="port" value="6379"/> <property name="database" value="0"/> </bean> <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="jedisConnectionFactory"/> <property name="keySerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/> </property> <property name="valueSerializer"> <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/> </property> </bean>

并在主applicationContext.xml中<import resource="applicationContext-redis.xml"/>。

第三步:改造UserDaoImpl,增加缓存逻辑

@Repository("userDao") public class UserDaoImpl implements UserDao { @Autowired private SessionFactory sessionFactory; @Autowired private RedisTemplate<String, Object> redisTemplate; // 注入RedisTemplate @Override public User findById(Long id) { String key = "user:" + id; // 1. 先查Redis User user = (User) redisTemplate.opsForValue().get(key); if (user != null) { System.out.println("Cache hit: " + key); return user; } // 2. Redis未命中,查DB Session session = sessionFactory.getCurrentSession(); user = (User) session.get(User.class, id); if (user != null) { // 3. 写入Redis,设置过期时间2小时(防雪崩) redisTemplate.opsForValue().set(key, user, 2, TimeUnit.HOURS); System.out.println("Cache miss, load from DB: " + key); } return user; } @Override public void save(User user) { Session session = sessionFactory.getCurrentSession(); session.save(user); // 4. 新增用户后,删除对应缓存(防脏读) redisTemplate.delete("user:" + user.getId()); } }

第四步:防御缓存穿透——对空结果也缓存
在findById方法中,当DB查询返回null时,写入一个特殊标记:

if (user == null) { // 缓存空对象,防止重复查询DB redisTemplate.opsForValue().set(key + ":null", "null", 5, TimeUnit.MINUTES); return null; } // 在开头增加空标记检查 if (redisTemplate.hasKey(key + ":null")) { return null; }

6.3 验证与压测:用JMeter确认缓存效果

用JMeter模拟100并发请求/userRegister.action?id=1,对比改造前后:

指标改造前改造后
平均响应时间120ms15ms
数据库QPS1005
错误率0%0%

关键观察点:首次请求日志有Cache miss,后续请求均为Cache hit;Redis中KEYS user:*应看到对应key;数据库慢查询日志不再出现该SQL。

从那以后我每次给老项目加缓存,都强制走一遍“空值缓存→过期时间分级→缓存删除时机”三重校验,宁可多写10行代码,也不让线上出现缓存雪崩。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 2:48:36

spotDL 快速上手:Spotify 歌曲离线保存成 MP3,有多简单?

spotDL 快速上手&#xff1a;Spotify 歌曲离线保存成 MP3&#xff0c;有多简单&#xff1f; 【免费下载链接】spotify-downloader Download your Spotify playlists and songs along with album art and metadata (from YouTube if a match is found). 项目地址: https://git…

作者头像 李华
网站建设 2026/10/18 2:46:41

链表算法面试全解析:虚拟头节点与快慢指针实战套路

我前一阵密集面了几家公司&#xff0c;发现一个挺有意思的现象&#xff1a;不管面试官是什么级别、什么方向&#xff0c;链表题几乎成了“标配开场”。尤其是准备过牛客面试热题101里链表系列之后&#xff0c;很多题你甚至不用等面试官开口&#xff0c;心里已经能预判他要考你哪…

作者头像 李华
网站建设 2026/10/18 2:41:52

【VMware虚拟机】安装【完整流程】

官网&#xff1a;Fusion and Workstation | VMware 本地资料&#xff1a; VMware17&#xff1a;【免费】VMw????????17资源-CSDN文库 VMware16&#xff1a;【免费】VM?????????????????????????????????????16资源-CSDN文库 …

作者头像 李华
网站建设 2026/10/18 2:36:19

LTX-2.5-GGUF商用指南:LTX-2.x社区许可协议与千万美元营收红线详解

【免费下载链接】LTX-2.5-GGUF 项目地址&#xff1a; https://ai.gitcode.com/hf_mirrors/unsloth/LTX-2.5-GGUF 点击查看 免费下载 LTX-2.5-GGUF 是 Lightricks 22B 音视频生成模型 LTX-2.5 的 GGUF 量化版本&#xff0c;支持文生视频、图生视频&#xff0c;可直接用于商业部…

作者头像 李华
网站建设 2026/10/18 2:36:03

自研代码审查辅助工具:从规则引擎到私有化部署的工程实践

我团队维护的代码库持续膨胀之后&#xff0c;每周的 PR 评审积压量开始成倍增加。与其继续靠人海战术熬夜重审&#xff0c;我们决定做一个内部代号为 open-code-review 的代码审查辅助服务&#xff0c;用自动化手段把高重复性的检查工作前置。这套东西说穿了不复杂&#xff0c;…

作者头像 李华