☰
Struts2+Spring+Hibernate2老项目运行与调试指南
2026/10/19 8:02:47 网站建设 项目流程

简介:本资源是一份面向Java Web开发初学者与中级工程师的SSH2框架整合实战指南,聚焦Struts2、Spring、Hibernate三大主流框架在MyEclipse环境下的协同配置与工程落地。资源提供完整可运行的整合项目源码及配套详解文档,有效解决框架版本兼容、分层结构搭建、配置文件联动(applicationContext.xml、struts.xml、hibernate.cfg.xml等)等典型整合难点。压缩包共54个文件,含26个核心jar依赖库、9个关键XML配置文件、5个Java业务类、2个JSP视图页及1个详尽的Word版整合说明文档,整体14.71MB,结构清晰、开箱即用。目前已有623人学习下载,读者可直接导入MyEclipse调试运行,深入理解Action→Service→DAO的调用链路、Spring容器管理Bean的机制,以及Hibernate对象关系映射与事务控制的实际实现方式。

1. 这不是SSH协议的Java实现,而是Struts2+Spring+Hibernate2老项目整合的“考古现场”

你点开这个资源,第一反应可能是:SSH2?OpenSSH?JSch?不对——它压根不碰网络协议层。这是2008年前后国内Java Web开发黄金期的典型产物:Struts2(S)、Spring(S)、Hibernate2(H)三大框架在Tomcat 5.5/6环境下手工缝合的完整MVC工程。它没有Maven坐标、没有Spring Boot自动装配、没有Lombok,连log4j配置都写在properties里。但正因如此,它成了理解传统Java EE分层架构最干净的“解剖标本”:Controller怎么把请求甩给Service,Service怎么用Spring事务管理Hibernate Session,DAO层怎么手写HQL与映射文件联动。如果你正在维护遗留系统、准备面试老派Java岗,或想亲手拧紧每一颗螺丝理解IoC/AOP/ORM如何在XML时代协同工作——这份源码就是你的调试器断点集。它不炫技,但每个包名、每个xml标签、每行<bean id="userDao" class="...">都在讲一个被现代框架封装掉的故事。


2. 环境搭建与运行:从JDK 1.5到Tomcat 6的精准复刻

2.1 JDK与容器版本必须卡死:为什么不能用JDK 17跑通?

这个项目编译目标字节码是1.5(<maven.compiler.target>1.5</maven.compiler.target>在pom.xml中不存在,因为压根没用Maven),所有泛型擦除、for-each语法糖、注解处理器都按JDK 1.5规范生成。若强行用JDK 17启动,你会在类加载阶段就遇到UnsupportedClassVersionError——这不是报错,是JVM直接拒绝读取class文件。更隐蔽的是Spring 2.5.6对JDK 1.5的反射API强依赖:比如org.springframework.util.ClassUtils.isAssignable()内部调用Class.isAssignableFrom()时,JDK 17的模块化检查会拦截非模块路径下的类加载,导致ApplicationContext初始化失败。解决方案只有两个:装JDK 1.5(已淘汰)或降级到JDK 1.6(实测兼容性最佳)。我本地用JDK 1.6.0_45 + Tomcat 6.0.39组合,启动日志里出现INFO: Server startup in [xxx] ms即为成功。

# 检查JDK版本(必须显示1.6.0_xxx) java -version # Tomcat启动前确认CATALINA_HOME指向正确路径 echo $CATALINA_HOME # 输出应为 /opt/tomcat6(示例路径) # 部署war包(注意不是ROOT.war,是ssh2_demo.war) cp ssh2_demo.war $CATALINA_HOME/webapps/ $CATALINA_HOME/bin/startup.sh

提示:Tomcat 6的conf/server.xml需确认<Connector port="8080" protocol="HTTP/1.1"/>未被注释,且webapps/目录下无残留的ssh2_demo文件夹(否则war解压失败)。

2.2 数据库连接三步走:MySQL驱动、schema初始化、Hibernate方言校准

项目用MySQL 5.0+,但Hibernate2默认方言是org.hibernate.dialect.MySQLDialect,它生成的SQL不支持LIMIT子句(MySQL 5.0才引入)。若数据库是5.1+,必须升级为MySQL5Dialect,否则分页查询直接报错。驱动jar必须是mysql-connector-java-3.1.12-bin.jar(项目lib目录下已提供),新版驱动(如8.x)会因包路径变更(com.mysql.cj.jdbc.Driver→com.mysql.jdbc.Driver)导致Class.forName()失败。

<!-- src/hibernate.cfg.xml 中关键配置 --> <property name="dialect">org.hibernate.dialect.MySQL5Dialect</property> <property name="connection.driver_class">com.mysql.jdbc.Driver</property> <property name="connection.url">jdbc:mysql://localhost:3306/ssh2_demo?useUnicode=true&amp;characterEncoding=UTF-8</property> <property name="connection.username">root</property> <property name="connection.password">123456</property>

数据库初始化脚本在doc/ssh2_demo.sql中,执行前需手动创建数据库:

CREATE DATABASE ssh2_demo CHARACTER SET utf8 COLLATE utf8_general_ci; -- 然后source doc/ssh2_demo.sql

注意:SQL脚本中user表含password字段,但Hibernate映射文件User.hbm.xml里对应property为pwd,这是典型命名不一致坑——后续改造时必须同步修改。

2.3 Struts2核心拦截器链验证:Action执行前发生了什么?

项目用Struts2 2.0.11,其拦截器栈定义在struts.xml中:

<package name="default" extends="struts-default"> <interceptors> <interceptor-stack name="myStack"> <interceptor-ref name="exception"/> <interceptor-ref name="alias"/> <interceptor-ref name="servletConfig"/> <interceptor-ref name="prepare"/> <interceptor-ref name="i18n"/> <interceptor-ref name="chain"/> <interceptor-ref name="debugging"/> <interceptor-ref name="profiling"/> <interceptor-ref name="scopedModelDriven"/> <interceptor-ref name="modelDriven"/> <interceptor-ref name="fileUpload"/> <interceptor-ref name="checkbox"/> <interceptor-ref name="staticParams"/> <interceptor-ref name="params"> <param name="excludeParams">dojo\..*,^struts\..*</param> </interceptor-ref> <interceptor-ref name="conversionError"/> <interceptor-ref name="validation"> <param name="excludeMethods">input,back,cancel,browse</param> </interceptor-ref> <interceptor-ref name="workflow"> <param name="excludeMethods">input,back,cancel,browse</param> </interceptor-ref> </interceptor-stack> </interceptors> </package>

当访问/user_list.action时,请求会按此顺序执行拦截器。最关键的params拦截器负责将HTTP参数注入Action属性(如username=abc→userAction.setUsername("abc")),而modelDriven拦截器则把User对象设为值栈根对象。验证方法:在UserAction.java的execute()方法首行加System.out.println(ActionContext.getContext().getValueStack().getRoot());,启动后访问页面,控制台会打印出[com.xxx.model.User@xxxx, com.xxx.action.UserAction@xxxx]——证明ModelDriven生效。


3. 核心模块拆解:一个用户列表请求从Struts2 Action到Hibernate SQL的完整链路

3.1 Struts2 Action层:UserAction如何接收参数并委托业务逻辑

UserAction.java是入口,继承ActionSupport并实现ModelDriven<User>:

public class UserAction extends ActionSupport implements ModelDriven<User> { private User user = new User(); // 模型对象 private UserService userService; // Spring注入的Service public String list() { // 对应/user_list.action try { userList = userService.findAll(); // 委托给Service return SUCCESS; } catch (Exception e) { addActionError("查询用户列表失败:" + e.getMessage()); return ERROR; } } @Override public User getModel() { return user; // ModelDriven契约:返回模型对象供params拦截器注入 } }

关键点在于getModel()返回的user实例会被params拦截器自动填充。例如URL带?username=admin&age=25,user.setUsername("admin")和user.setAge(25)会自动调用。但注意:userList是Action的成员变量,用于存查询结果并传递给JSP,它不会被自动注入——这是初学者常混淆的点。

3.2 Spring Service层:事务边界与Hibernate Session管理

UserServiceImpl.java通过Spring AOP声明式事务控制数据库操作:

public class UserServiceImpl implements UserService { private UserDao userDao; // Spring注入的DAO @Transactional(readOnly = true) // 事务注解(Spring 2.5+支持) public List<User> findAll() { return userDao.findAll(); // 调用DAO方法 } @Transactional // 默认propagation=REQUIRED public void save(User user) { userDao.save(user); } }

applicationContext.xml中配置了事务管理器:

<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>

重点:HibernateTransactionManager绑定的是sessionFactory,而非dataSource。这意味着事务提交时,Hibernate会flush一级缓存并同步到数据库,而不是直接执行JDBC commit。若你在DAO中手动调用session.beginTransaction(),会与Spring事务冲突导致NestedTransactionsNotSupportedException。

3.3 Hibernate DAO层:HQL查询与映射文件的双向绑定

UserDaoImpl.java使用Hibernate Template简化Session操作:

public class UserDaoImpl extends HibernateDaoSupport implements UserDao { public List<User> findAll() { return getHibernateTemplate().find("from User"); // HQL查询 } public void save(User user) { getHibernateTemplate().save(user); } }

对应的User.hbm.xml定义了对象-关系映射:

<class name="com.xxx.model.User" table="user"> <id name="id" column="id"> <generator class="native"/> <!-- MySQL用auto_increment --> </id> <property name="username" column="username" type="string"/> <property name="pwd" column="password" type="string"/> <!-- 注意:DB字段password ↔ Java属性pwd --> <property name="age" column="age" type="integer"/> </class>

玄学时刻:<generator class="native"/>在MySQL中实际生成identity策略,但Hibernate2文档明确说native会根据方言选择identity/sequence/hilo。实测中若MySQL表id未设AUTO_INCREMENT,save操作会抛IdentifierGenerationException——这是必须检查的建表语句细节。


4. 避坑指南:五个让老开发者拍桌的典型翻车现场

4.1 现象:访问/user_list.action返回404,Tomcat日志无Struts2初始化信息

原因:web.xml中Struts2过滤器映射路径错误。项目用*.action后缀,但<filter-mapping>写成<url-pattern>/user/*</url-pattern>,导致只有/user/xxx路径被拦截,/user_list.action被Tomcat默认Servlet处理。
解决:修正为<url-pattern>*.action</url-pattern>,并确认<filter-class>是org.apache.struts2.dispatcher.FilterDispatcher(Struts2 2.0.x的类名,非2.5+的StrutsPrepareAndExecuteFilter)。

4.2 现象:登录成功后跳转/main.jsp,但页面显示java.lang.NoClassDefFoundError: org/apache/commons/logging/LogFactory

原因:commons-logging-1.0.4.jar缺失。Struts2 2.0.11依赖此版本,新版commons-logging-1.2.jar因包结构变更(org.apache.commons.logging→org.apache.commons.logging.impl)导致类加载失败。
解决:从项目lib/目录复制commons-logging-1.0.4.jar到WEB-INF/lib/,删除其他版本。

4.3 现象:UserAction中userService为null,list()方法空指针

原因:Spring未扫描到Action类。applicationContext.xml中<context:component-scan>未包含com.xxx.action包,或UserAction未用@Service注解(项目用XML配置,故需在XML中显式声明bean)。
解决:在applicationContext.xml添加:

<bean name="userAction" class="com.xxx.action.UserAction" scope="prototype"> <property name="userService" ref="userService"/> </bean>

并确保struts.xml中Action配置引用该bean:

<action name="user_list" class="userAction" method="list"> <result name="success">/user/list.jsp</result> </action>

4.4 现象:MySQL中文乱码,JSP显示????

原因:三处编码未统一:① JDBC URL缺useUnicode=true&characterEncoding=UTF-8;②web.xml中CharacterEncodingFilter未配置;③ JSP页面<%@ page contentType="text/html;charset=UTF-8"%>缺失。
解决:补全上述三处,特别注意CharacterEncodingFilter需在StrutsPrepareAndExecuteFilter之前(但本项目用FilterDispatcher,故顺序为CharacterEncodingFilter→FilterDispatcher)。

4.5 现象:Hibernate执行save()后数据库无数据,且无异常

原因:事务未提交。UserServiceImpl的save()方法缺少@Transactional注解,或applicationContext.xml中<tx:annotation-driven>未启用,导致DAO操作在无事务上下文中执行,Session flush后未commit。
解决:确认@Transactional存在,并检查Spring配置是否加载——在web.xml中ContextLoaderListener的contextConfigLocation是否指向applicationContext.xml。


5. 二次开发实战:给用户管理模块增加条件查询与分页功能

5.1 前端JSP改造:添加查询表单与分页控件

在/user/list.jsp顶部插入查询表单:

<form action="user_search.action" method="post"> 用户名:<input type="text" name="username" value="${param.username}"/> 年龄范围:<input type="text" name="minAge" value="${param.minAge}"/> ~ <input type="text" name="maxAge" value="${param.maxAge}"/> <input type="submit" value="搜索"/> </form>

分页区域替换原<c:forEach>循环:

<c:forEach items="${pageBean.list}" var="u"> <tr><td>${u.username}</td><td>${u.age}</td></tr> </c:forEach> <div class="pagination"> 共${pageBean.totalCount}条 | 第${pageBean.currentPage}页/共${pageBean.totalPage}页 <a href="user_list.action?pageNum=1">首页</a> <a href="user_list.action?pageNum=${pageBean.currentPage-1 > 0 ? pageBean.currentPage-1 : 1}">上一页</a> <a href="user_list.action?pageNum=${pageBean.currentPage+1 <= pageBean.totalPage ? pageBean.currentPage+1 : pageBean.totalPage}">下一页</a> <a href="user_list.action?pageNum=${pageBean.totalPage}">末页</a> </div>

5.2 后端Action与Service扩展:支持动态HQL与分页计算

UserAction.java新增search()方法:

private String username; private Integer minAge; private Integer maxAge; private PageBean pageBean = new PageBean(); // 自定义分页对象 public String search() { try { // 构建动态HQL条件 StringBuilder hql = new StringBuilder("from User where 1=1"); List<Object> params = new ArrayList<>(); if (StringUtils.isNotBlank(username)) { hql.append(" and username like ?"); params.add("%" + username + "%"); } if (minAge != null) { hql.append(" and age >= ?"); params.add(minAge); } if (maxAge != null) { hql.append(" and age <= ?"); params.add(maxAge); } // 计算总记录数(用于分页) Long totalCount = (Long) getHibernateTemplate() .find("select count(*) " + hql.toString(), params.toArray()).get(0); pageBean.setTotalCount(totalCount.intValue()); // 查询当前页数据 List<User> list = getHibernateTemplate() .find(hql.toString() + " order by id desc", params.toArray()); pageBean.setList(list); return SUCCESS; } catch (Exception e) { addActionError("查询失败:" + e.getMessage()); return ERROR; } }

PageBean.java需实现基本分页属性:

public class PageBean { private int totalCount; // 总记录数 private int pageSize = 10; // 每页数量 private int totalPage; // 总页数 private int currentPage = 1; // 当前页 private List<?> list; // 当前页数据 public void setTotalCount(int totalCount) { this.totalCount = totalCount; this.totalPage = (int) Math.ceil((double) totalCount / pageSize); } // getter/setter省略... }

5.3 Hibernate DAO层优化:避免N+1查询问题

原findAll()方法直接getHibernateTemplate().find("from User"),若User关联Role表,会触发N+1查询(1次查User,N次查Role)。改用JOIN FETCH一次性加载:

// UserDaoImpl.java public List<User> findAllWithRoles() { return getHibernateTemplate().find( "from User u join fetch u.roles" // 假设User.hbm.xml有<set name="roles" fetch="join"> ); }

对应User.hbm.xml需声明关联映射:

<set name="roles" table="user_role" inverse="true" lazy="false" fetch="join"> <key column="user_id"/> <many-to-many column="role_id" class="com.xxx.model.Role"/> </set>

从那以后我每次接手老项目,第一件事就是用tcpdump抓包看Hibernate生成的SQL(加show_sql=true参数),再对照log4j.properties里log4j.logger.org.hibernate.SQL=DEBUG的日志,确保每条业务查询只发1次SQL。这比读100页文档更能看清数据流向。希望帮到你。

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

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

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

立即咨询