资讯动态

SSH框架整合CRUD实战:Struts2+Spring+Hibernate增删改查完整教程

发布时间:2026/10/14 20:47:33 来源:尧图企业网站定制
简介这份资源是 Struts2.1、Spring 2.5 与 Hibernate 3.2 三大经典框架整合实现增删改查的完整示例工程面向正在学习 Java Web 框架整合的初学者与需要搭建企业级应用骨架的开发者。包内共 88 个文件以 12 个 java 源码、12 个 xml 配置、14 个 class 编译文件及 12 个 repository 等为主涵盖 Action、Service、DAO 分层代码与 struts.xml、applicationContext.xml、hibernate 映射等关键配置另有 jsp 页面与 properties 资源文件压缩包约 42KB结构紧凑便于对照阅读。目前已有 308 人学习下载。通过该示例可掌握 Struts2 请求转发、Spring 依赖注入与事务管理、Hibernate 对象持久化三者协同的完整链路理解书籍信息的新增、删除、修改与查询如何从页面请求贯通到数据库操作是入门 SSH 整合开发、理清分层职责与配置要点的实用参考。1. 从一份老项目源码包说起SSH 三件套的 CRUD 到底长什么样如果你手里有一份标注着 struts2.1 spring 2.5 hibernate 3.2 的增删改查源码包第一反应大概率是「这版本也太老了」。但先别急着关掉——这套组合在国内企业级开发里统治了将近十年大量存量系统至今还在跑很多公司的老平台、内部管理系统、甚至一些行业软件的底层仍然是这套架构。你接手维护、二次开发、或者单纯想搞懂「为什么老项目这么写」都绕不开它。这份资源解决的核心问题很具体给你一套能跑通的最小闭环从 Action 接收请求到 Service 事务控制再到 DAO 层 Hibernate 映射数据库完整走一遍增删改查。适合三类人一是刚接触 Java Web、需要理解分层架构的新手二是被派去维护老系统、需要快速上手的开发者三是想对比「当年为什么这么设计」的技术负责人。它不炫技但每一层都有明确的职责边界读懂了再看现在的 Spring Boot MyBatis很多设计思路的来龙去脉就清楚了。2. 环境搭建与依赖版本对齐别让 jar 包打架2.1 为什么这三个版本必须锁死Struts2.1、Spring 2.5、Hibernate 3.2 这三个版本是经过大量项目验证的稳定组合但它们对依赖 jar 的版本非常敏感。常见做法是直接使用资源包里 lib 目录下的 jar不要自作主张去 Maven 中央仓库拉最新版。我见过太多人因为把 commons-lang 从 2.3 升到 2.6导致 Struts 的 OGNL 表达式解析直接报 NoSuchMethodError排查半天才发现是版本冲突。核心依赖清单如下建议对照检查依赖包推荐版本作用冲突风险struts2-core2.1.8MVC 核心与 spring-web 版本不匹配会报 ClassNotFoundspring-core2.5.6IoC 容器与 cglib 版本绑定hibernate33.2.7.gaORM 映射与 javassist 版本强绑定commons-logging1.1.1日志桥接与 log4j 同时存在时需排除javassist3.9.0.GA字节码增强Hibernate 3.2 必须用这个版本提示如果你用 Maven 管理建议把资源包里的 jar 先 install 到本地仓库再在 pom 里写死版本号不要用 LATEST 或 RELEASE。2.2 web.xml 里的三处关键配置环境搭好后web.xml 是第一个容易翻车的地方。Struts2 的过滤器、Spring 的监听器、Hibernate 的 Session 管理三者的加载顺序有讲究。!-- web.xml 核心片段 -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param !-- Spring 监听器必须放在 Struts 过滤器之前 -- listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener !-- OpenSessionInView 过滤器解决懒加载问题 -- filter filter-namehibernateFilter/filter-name filter-classorg.springframework.orm.hibernate3.support.OpenSessionInViewFilter/filter-class /filter filter-mapping filter-namehibernateFilter/filter-name url-pattern/*/url-pattern /filter-mapping !-- Struts2 过滤器放最后 -- filter filter-namestruts2/filter-name filter-classorg.apache.struts2.dispatcher.FilterDispatcher/filter-class /filter filter-mapping filter-namestruts2/filter-name url-pattern/*/url-pattern /filter-mapping逻辑说明ContextLoaderListener 负责启动 Spring 容器OpenSessionInViewFilter 保证一次请求内 Session 不关闭Struts2 的 FilterDispatcher 负责路由。顺序错了会出现「Spring 容器还没初始化Struts Action 已经创建」的经典报错。参数上contextConfigLocation 指向的配置文件路径要和实际目录一致放在 src 根目录下编译后就在 classpath 根下。2.3 数据库连接与方言配置Hibernate 3.2 的方言配置是另一个高频踩坑点。MySQL 5.x 用 MySQL5Dialect但如果你连的是 MySQL 8驱动包和方言都要换否则启动就报 Unknown entity 或者连接超时。!-- applicationContext.xml 中的数据源与 SessionFactory -- bean iddataSource classorg.apache.commons.dbcp.BasicDataSource destroy-methodclose property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/ssh_demo?useUnicodetrueamp;characterEncodingutf8/ property nameusername valueroot/ property namepassword value123456/ property namemaxActive value20/ property namemaxIdle value5/ /bean bean idsessionFactory classorg.springframework.orm.hibernate3.LocalSessionFactoryBean property namedataSource refdataSource/ property namemappingResources list valuecom/demo/entity/User.hbm.xml/value /list /property property namehibernateProperties props prop keyhibernate.dialectorg.hibernate.dialect.MySQL5Dialect/prop prop keyhibernate.show_sqltrue/prop prop keyhibernate.format_sqltrue/prop prop keyhibernate.hbm2ddl.autoupdate/prop /props /property /bean参数说明maxActive 控制连接池上限老项目并发不高设 20 够用hbm2ddl.auto 设 update 可以自动建表但生产环境建议改成 none手动执行 DDL。show_sql 和 format_sql 调试期打开上线关掉否则日志量会爆炸。3. 分层代码落地Action、Service、DAO 各管什么3.1 Action 层接收参数与结果跳转Struts2 的 Action 是请求入口它通过 setter 注入接收表单参数通过 execute 方法返回结果字符串。Spring 2.5 管理 Action 的方式是在 struts.xml 里把 class 指向 Spring 容器中的 bean 名称。// UserAction.java public class UserAction extends ActionSupport { private User user; private ListUser userList; private UserService userService; // Spring 注入 Service public void setUserService(UserService userService) { this.userService userService; } public String list() { userList userService.findAll(); return list; } public String add() { userService.save(user); return redirectList; } public String delete() { userService.deleteById(user.getId()); return redirectList; } public String edit() { user userService.findById(user.getId()); return edit; } public String update() { userService.update(user); return redirectList; } // getter/setter 省略 }逻辑说明list 方法查全部返回列表页add/delete/update 执行后重定向回列表避免刷新重复提交。edit 先查再跳编辑页。参数上user 对象由 Struts2 的参数拦截器自动封装前提是表单字段名和 User 属性名一致。struts.xml 的配置要点struts constant namestruts.objectFactory valuespring/ package nameuser extendsstruts-default namespace/user action namelist classuserAction methodlist result namelist/WEB-INF/jsp/userList.jsp/result /action action nameadd classuserAction methodadd result nameredirectList typeredirect/user/list.action/result /action action namedelete classuserAction methoddelete result nameredirectList typeredirect/user/list.action/result /action action nameedit classuserAction methodedit result nameedit/WEB-INF/jsp/userEdit.jsp/result /action action nameupdate classuserAction methodupdate result nameredirectList typeredirect/user/list.action/result /action /package /struts关键点struts.objectFactory 设为 spring 后class 属性写的是 Spring bean 的 id不是全限定类名。redirect 类型的结果会发起新请求浏览器地址栏变化这是和 dispatcher 的本质区别。3.2 Service 层事务边界与业务组装Service 层是事务的边界Spring 2.5 用 XML 配置声明式事务最稳妥。事务方法命名建议用 save、delete、update 开头方便用通配符匹配。// UserServiceImpl.java public class UserServiceImpl implements UserService { private UserDao userDao; public void setUserDao(UserDao userDao) { this.userDao userDao; } public ListUser findAll() { return userDao.findAll(); } public void save(User user) { userDao.save(user); } public void deleteById(Long id) { userDao.deleteById(id); } public User findById(Long id) { return userDao.findById(id); } public void update(User user) { userDao.update(user); } }事务配置!-- applicationContext.xml 事务部分 -- bean idtransactionManager classorg.springframework.orm.hibernate3.HibernateTransactionManager property namesessionFactory refsessionFactory/ /bean bean idtxProxyTemplate abstracttrue classorg.springframework.transaction.interceptor.TransactionProxyFactoryBean property nametransactionManager reftransactionManager/ property nametransactionAttributes props prop keysave*PROPAGATION_REQUIRED/prop prop keydelete*PROPAGATION_REQUIRED/prop prop keyupdate*PROPAGATION_REQUIRED/prop prop keyfind*PROPAGATION_REQUIRED,readOnly/prop /props /property /bean bean iduserService parenttxProxyTemplate property nametarget bean classcom.demo.service.impl.UserServiceImpl property nameuserDao refuserDao/ /bean /property /bean参数说明PROPAGATION_REQUIRED 表示当前有事务就加入没有就新建readOnly 告诉 Hibernate 这是只读操作可以优化性能。find 方法加 readOnly 是血泪经验不加的话 Hibernate 会做脏检查大列表查询时内存和 CPU 都吃不消。3.3 DAO 层Hibernate 模板与映射文件DAO 层用 HibernateTemplate 可以省去手动管理 Session 的麻烦Spring 2.5 对它的封装已经很成熟。// UserDaoImpl.java public class UserDaoImpl extends HibernateDaoSupport implements UserDao { public ListUser findAll() { return getHibernateTemplate().find(from User order by id desc); } public void save(User user) { getHibernateTemplate().save(user); } public void deleteById(Long id) { User user (User) getHibernateTemplate().get(User.class, id); if (user ! null) { getHibernateTemplate().delete(user); } } public User findById(Long id) { return (User) getHibernateTemplate().get(User.class, id); } public void update(User user) { getHibernateTemplate().update(user); } }HibernateDaoSupport 需要注入 sessionFactory配置如下bean iduserDao classcom.demo.dao.impl.UserDaoImpl property namesessionFactory refsessionFactory/ /bean实体映射文件 User.hbm.xmlhibernate-mapping class namecom.demo.entity.User tablet_user id nameid typejava.lang.Long column nameid/ generator classnative/ /id property nameusername typejava.lang.String column nameusername length50 not-nulltrue/ /property property namepassword typejava.lang.String column namepassword length50 not-nulltrue/ /property property nameemail typejava.lang.String column nameemail length100/ /property /class /hibernate-mappinggenerator 用 native 让数据库自己决定主键策略MySQL 下就是 auto_increment。not-null 约束在数据库层和 Hibernate 层都生效插入 null 会直接抛异常比等到数据库报错更早发现问题。4. 增删改查实操从页面到数据库的完整链路4.1 列表页与分页的取舍列表页用 JSP 加 JSTL 遍历 userList这是最直接的做法。但数据量大了必须分页Hibernate 3.2 的分页用 Query 对象的 setFirstResult 和 setMaxResults。// 分页查询示例 public ListUser findByPage(int pageNo, int pageSize) { String hql from User order by id desc; Query query getSession().createQuery(hql); query.setFirstResult((pageNo - 1) * pageSize); query.setMaxResults(pageSize); return query.list(); }逻辑说明setFirstResult 是起始行索引从 0 开始setMaxResults 是每页条数。pageNo 从 1 开始所以减 1 再乘 pageSize。注意 getSession 方法来自 HibernateDaoSupport它拿到的 Session 受 OpenSessionInViewFilter 管理不要手动 close。4.2 新增与表单重复提交新增操作最大的坑是刷新页面导致重复插入。解决方案是重定向add 方法执行完后返回 redirectList浏览器发起新的 GET 请求刷新也只是重新查列表。public String add() { if (user.getUsername() null || user.getUsername().trim().isEmpty()) { addActionError(用户名不能为空); return input; } userService.save(user); return redirectList; }参数校验放在 Service 之前空值直接返回 input 结果struts.xml 里配置 input 指向新增页面。这样用户看到错误提示不用重新填整个表单。4.3 删除的两种策略与级联问题物理删除直接调 delete逻辑删除改状态字段。老项目里逻辑删除更常见因为数据往往有审计要求。// 逻辑删除示例 public void logicDelete(Long id) { User user (User) getHibernateTemplate().get(User.class, id); if (user ! null) { user.setStatus(0); // 0 表示已删除 getHibernateTemplate().update(user); } }如果 User 和其他实体有关联删除时会报外键约束异常。常见做法是在映射文件里配置 cascade但级联删除风险高我一般建议手动先删子表再删主表逻辑清晰可控。4.4 修改与乐观锁修改操作在并发场景下会有覆盖问题。Hibernate 3.2 支持乐观锁在实体里加 version 字段即可。property nameversion typejava.lang.Integer column nameversion/ /property同时在 class 标签上加 optimistic-lockversion。这样两个用户同时编辑同一条记录后提交的会抛 StaleObjectStateException前端提示「数据已被他人修改请刷新重试」。不加这个字段后提交的直接覆盖前者数据就丢了。5. 避坑与排查那些年我们踩过的 SSH 雷区5.1 懒加载异常could not initialize proxy现象页面渲染时抛 LazyInitializationException提示 no session or session was closed。原因OpenSessionInViewFilter 没配或者配在了 Struts2 过滤器后面导致 Action 执行完 Session 就关了JSP 渲染时再去取关联对象就报错。解决确保 OpenSessionInViewFilter 在 web.xml 里排在 Struts2 过滤器之前且 url-pattern 覆盖所有请求。如果还不行检查 filter 的 init-param 里是否配了 singleSession默认 true 即可。5.2 事务不生效save 成功但数据库没数据现象Action 返回成功日志也打印了 insert 语句但数据库里查不到。原因Service 没有被事务代理包裹Hibernate 的 flush 没触发。常见于手动 new 了 ServiceImpl而不是从 Spring 容器里拿。解决检查 struts.xml 里 action 的 class 是否指向 Spring bean id以及 applicationContext.xml 里 userService 是否配置了 txProxyTemplate 父 bean。用 getBean 方式获取 Service 时类型要写接口而不是实现类否则 JDK 动态代理会失败。5.3 中文乱码POST 和 GET 表现不一样现象表单提交的中文存到数据库变成问号GET 请求参数乱码。原因POST 乱码是 request 编码问题GET 乱码是服务器 URIEncoding 问题。解决POST 在 web.xml 加 CharacterEncodingFilter设 encoding 为 UTF-8forceEncoding 为 true放在所有过滤器最前面。GET 需要改 Tomcat 的 server.xml在 Connector 标签加 URIEncodingUTF-8。数据库连接 URL 里也要带 useUnicodetruecharacterEncodingutf8。5.4 jar 冲突NoSuchMethodError 和 ClassNotFoundException现象启动报 NoSuchMethodError或者运行到某个功能才报 ClassNotFoundException。原因同一个类有多个版本或者缺少传递依赖。解决用 mvn dependency:tree 打印依赖树找出版本冲突的包用 exclusions 排除。老项目不用 Maven 的话把 WEB-INF/lib 下的 jar 按名称排序同名不同版本的只留一个。javassist、cglib、asm 这三个最容易冲突重点检查。5.5 事务嵌套PROPAGATION_REQUIRED 的边界现象Service A 调用 Service BB 抛异常但 A 的事务没回滚。原因B 的事务传播行为配成了 REQUIRES_NEW或者异常被 catch 了没往外抛。解决默认都用 PROPAGATION_REQUIRED让它们共享同一个事务。如果确实需要独立事务明确用 REQUIRES_NEW并且注意异常处理。Spring 默认只对 RuntimeException 回滚检查异常需要显式配置 rollbackFor。6. 进阶技巧用拦截器统一处理日志与权限老项目里最实用的进阶玩法是自定义 Struts2 拦截器把日志和权限校验从 Action 里抽出来。这样每个 Action 只关心业务横切逻辑集中管理。// LogInterceptor.java public class LogInterceptor implements Interceptor { private static final Logger log Logger.getLogger(LogInterceptor.class); public String intercept(ActionInvocation invocation) throws Exception { String actionName invocation.getProxy().getActionName(); long start System.currentTimeMillis(); try { String result invocation.invoke(); log.info(actionName 执行成功耗时 (System.currentTimeMillis() - start) ms); return result; } catch (Exception e) { log.error(actionName 执行异常, e); throw e; } } public void destroy() {} public void init() {} }在 struts.xml 里注册拦截器栈interceptors interceptor namelogInterceptor classcom.demo.interceptor.LogInterceptor/ interceptor-stack namemyStack interceptor-ref namedefaultStack/ interceptor-ref namelogInterceptor/ /interceptor-stack /interceptors default-interceptor-ref namemyStack/逻辑说明defaultStack 是 Struts2 默认的拦截器集合包含参数封装、校验、国际化等。自定义拦截器加在后面invoke 之前做前置处理之后做后置处理。参数上getActionName 拿到的是 struts.xml 里 action 的 name 属性不是类名。权限拦截器同理在 invoke 之前检查 Session 里的用户信息没登录直接返回 login 结果。这样所有需要登录的 Action 统一配置不用每个方法里写一遍。验证拦截器是否生效最直接的方法是看日志输出顺序。如果日志在 Action 的 execute 之前打印说明前置逻辑执行了如果异常时日志也打印了说明 catch 块生效。我一般会在拦截器里加一个 traceId每次请求生成一个 UUID贯穿所有日志排查问题时能快速串起一次请求的完整链路。从那以后我每次接手老项目第一件事就是确认拦截器栈和事务配置这两处对了后面基本不会出大问题。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑