☰
SSH框架整合实战:从零搭建可运行的Java Web项目
2026/10/13 1:35:21 网站建设 项目流程

1. 从零搭建一个能跑通的SSH项目,到底要解决哪些问题

很多人第一次接触SSH框架,打开教程就开始复制粘贴配置文件,结果Spring的bean和Struts的action互相找不到,Hibernate的SessionFactory启动就报错,折腾一整天连个登录页面都跑不起来。这个场景太常见了。SSH这三个字母放在一起,指的是Spring + Struts + Hibernate这套经典的Java Web开发组合,虽然现在新项目用Spring Boot居多,但大量存量系统、教学项目、企业老平台仍然跑在这套架构上,能把SSH搭明白,对理解Java Web的分层思想、IoC容器、ORM映射这些核心概念有实打实的帮助。

这篇文章要做的,不是给你扔一堆XML让你自己猜,而是把搭建一个完整SSH示例项目的全过程拆开,从依赖选型、分层设计、配置整合到启动验证,每一步都讲清楚为什么这么做、不这么做会出什么问题。适合两类人看:一类是正在学Java Web、需要交课程设计或做练手项目的学生;另一类是工作中突然要维护一套老SSH系统、需要快速把环境跑起来的开发者。我会用Maven做依赖管理,用MySQL做数据库,整个项目结构清晰、可直接复现。

先说清楚一个前提:SSH的版本组合非常关键。Spring 4.x和Struts 2.3.x、Hibernate 4.x或5.x搭配比较稳,如果你用Spring 5配Struts 2.5,很多整合类的包名和API都变了,网上大部分教程直接失效。我下面给出的版本组合是经过实际验证能跑通的,你可以直接抄。

2. 版本选型与依赖矩阵:为什么这套组合最不容易翻车

2.1 三个框架的版本兼容性分析

SSH整合最大的坑不在代码,在版本。Spring负责管理bean和事务,Struts负责MVC的请求分发,Hibernate负责数据持久化,三者通过整合包桥接。整合包本身也有版本,比如struts2-spring-plugin,它的版本必须和Struts主版本一致,否则会出现Action无法注入Spring bean的问题。

我推荐的组合如下:

框架版本选择理由
Spring4.3.30.RELEASE4.x最后一个稳定版,整合包生态最全
Struts2.3.372.3.x系列成熟,与Spring 4整合文档最多
Hibernate4.3.11.Final与Spring 4的ORM模块配合稳定
MySQL驱动5.1.49兼容MySQL 5.7,避免8.x驱动的时区问题
JDK1.8SSH框架对JDK 8支持最好,JDK 11+有反射限制

为什么不选最新版?因为Struts 2.5之后包结构大改,struts2-spring-plugin的整合方式变了,而Hibernate 5.4之后又引入了新的引导API。你如果三个都用最新,等于同时踩三个坑,排查成本指数级上升。用上面这套组合,网上90%的教程都能对上,出问题也好搜。

2.2 Maven依赖的完整配置与排包技巧

依赖冲突是SSH项目的头号杀手。最典型的是javassist这个包,Hibernate要用它做字节码增强,Struts也可能间接依赖它,版本不一致就会报ClassNotFoundException或NoSuchMethodError。解决办法是在pom里显式声明版本,并用exclusions排除传递依赖。

核心依赖配置如下:

<properties> <spring.version>4.3.30.RELEASE</spring.version> <struts.version>2.3.37</struts.version> <hibernate.version>4.3.11.Final</hibernate.version> </properties> <dependencies> <!-- Spring核心 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency> <!-- Struts2与Spring整合 --> <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-core</artifactId> <version>${struts.version}</version> </dependency> <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-spring-plugin</artifactId> <version>${struts.version}</version> </dependency> <!-- Hibernate核心 --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>${hibernate.version}</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>${hibernate.version}</version> </dependency> <!-- 数据库驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> <!-- 连接池 --> <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.5</version> </dependency> <!-- 日志 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.30</version> </dependency> </dependencies>

注意:struts2-spring-plugin会自动引入Spring的部分包,可能和你显式声明的版本冲突。建议在它下面加exclusions,把spring-core、spring-context等排除掉,统一用你声明的版本。

2.3 目录结构的约定与分层逻辑

SSH项目的目录结构不是随便定的,它直接对应三层架构:表现层(Struts Action)、业务层(Service)、持久层(DAO)。我习惯这样组织:

src/main/java/com/example/ action/ -- Struts的Action类 service/ -- 业务接口 service/impl/ -- 业务实现 dao/ -- 数据访问接口 dao/impl/ -- 数据访问实现 entity/ -- Hibernate实体类 src/main/resources/ applicationContext.xml -- Spring总配置 applicationContext-dao.xml -- DAO层配置 applicationContext-service.xml -- Service层配置 applicationContext-tx.xml -- 事务配置 struts.xml -- Struts配置 jdbc.properties -- 数据库连接参数 log4j.properties -- 日志配置 src/main/webapp/ WEB-INF/ web.xml -- 部署描述符 jsp/ -- JSP页面

为什么要把Spring配置拆成多个文件?因为一个几千行的applicationContext.xml根本没法维护。按层拆分后,改DAO配置不用动Service,改事务策略不用碰bean定义。这是从实际维护经验里总结出来的,不是教科书上的教条。

3. 分层配置的实操细节:每一层到底在配什么

3.1 web.xml里的三个关键监听器与过滤器

web.xml是SSH项目的入口,它要完成三件事:启动Spring容器、注册Struts过滤器、配置字符编码。顺序不能乱。

<!-- Spring容器启动监听器 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext*.xml</param-value> </context-param> <!-- Struts2核心过滤器 --> <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> <!-- 字符编码过滤器,必须放在Struts之前 --> <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> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

这里有个容易忽略的点:contextConfigLocation用了通配符applicationContext*.xml,这样Spring会自动加载所有以applicationContext开头的配置文件。但要注意,如果文件放在src/main/resources下,编译后会到WEB-INF/classes,用classpath:前缀才能找到。

字符编码过滤器必须放在Struts过滤器之前,否则Struts先处理了请求,编码已经定了,再过滤也没用。这个顺序问题我见过至少五个人踩过,表现是表单提交中文全是乱码。

3.2 Spring管理Hibernate的SessionFactory配置

Hibernate的SessionFactory是整个持久层的核心,交给Spring管理后,你不需要手动openSession和close,Spring会通过HibernateTemplate或直接注入SessionFactory来操作。

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="${jdbc.driver}"/> <property name="jdbcUrl" value="${jdbc.url}"/> <property name="user" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="maxPoolSize" value="20"/> <property name="minPoolSize" value="5"/> </bean> <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="packagesToScan" value="com.example.entity"/> <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> </props> </property> </bean>

packagesToScan是Spring 4整合Hibernate 4的便利特性,它会自动扫描指定包下的实体类,不需要再写hibernate.cfg.xml或逐个声明mapping。hbm2ddl.auto设为update可以在启动时自动建表或更新表结构,开发阶段很方便,但生产环境一定要改成validate或none,否则可能误删字段。

连接池用c3p0是因为它配置简单、和Spring整合成熟。maxPoolSize设20是经验值,小项目够用,太大反而浪费数据库连接。jdbc.properties里放连接参数,通过<context:property-placeholder>加载:

<context:property-placeholder location="classpath:jdbc.properties"/>

3.3 DAO层的两种写法与选择建议

DAO层有两种常见写法:继承HibernateDaoSupport,或者直接注入SessionFactory。前者是老派做法,后者更灵活。

继承方式:

public class UserDaoImpl extends HibernateDaoSupport implements UserDao { public User findByUsername(String username) { return (User) getHibernateTemplate() .find("from User where username = ?", username).get(0); } }

注入方式:

@Repository public class UserDaoImpl implements UserDao { @Autowired private SessionFactory sessionFactory; public User findByUsername(String username) { Session session = sessionFactory.getCurrentSession(); Query query = session.createQuery("from User where username = :name"); query.setParameter("name", username); return (User) query.uniqueResult(); } }

我推荐注入方式,因为getCurrentSession()拿到的是和当前事务绑定的Session,不需要手动管理生命周期,而且可以用HQL的命名参数,比位置参数更清晰。继承方式在Spring 4里已经不太推荐了,HibernateDaoSupport的setSessionFactory方法在注解配置下容易出问题。

注意:用getCurrentSession()必须在Spring事务配置里开启事务,否则会报No Session found for current thread。这是新手最常见的报错之一。

3.4 Service层的事务边界与传播行为

Service层是事务的边界。一个业务方法要么全成功,要么全回滚,这个边界就划在Service方法上。

<!-- 事务管理器 --> <bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory"/> </bean> <!-- 注解驱动事务 --> <tx:annotation-driven transaction-manager="transactionManager"/> <!-- 或者用AOP切面配置 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="save*" propagation="REQUIRED"/> <tx:method name="update*" propagation="REQUIRED"/> <tx:method name="delete*" propagation="REQUIRED"/> <tx:method name="find*" read-only="true"/> <tx:method name="*" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="servicePointcut" expression="execution(* com.example.service..*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="servicePointcut"/> </aop:config>

用AOP切面配置的好处是不用在每个Service类上加@Transactional注解,按方法名前缀自动匹配事务策略。find*方法设为read-only="true"可以优化性能,Hibernate会跳过脏检查。save*、update*、delete*用REQUIRED传播行为,表示如果当前没有事务就新建一个,有就加入。

这里有个细节:切点表达式execution(* com.example.service..*.*(..))里的..表示匹配子包,如果你把Service实现类放在service.impl下,这个表达式能覆盖到。但如果写成了com.example.service.*.*(..),就只匹配service包下的类,不包含子包,事务会失效。这个坑我踩过,排查了半天才发现是切点写窄了。

4. Struts与Spring的整合:Action里怎么注入Service

4.1 struts.xml的核心配置与通配符映射

Struts的配置文件负责URL到Action的映射。用通配符可以少写很多重复配置。

<struts> <constant name="struts.objectFactory" value="spring"/> <constant name="struts.devMode" value="true"/> <package name="default" extends="struts-default" namespace="/"> <action name="user_*" class="userAction" method="{1}"> <result name="success">/WEB-INF/jsp/user/{1}.jsp</result> <result name="input">/WEB-INF/jsp/user/{1}.jsp</result> </action> </package> </struts>

struts.objectFactory设为spring是关键,它告诉Struts用Spring的容器来创建Action实例,这样Action里才能注入Service。user_*这个通配符映射表示:访问user_login.action会调用userAction的login方法,成功后跳转到/WEB-INF/jsp/user/login.jsp。一个配置覆盖了增删改查所有操作。

class="userAction"这里的userAction不是类全名,而是Spring容器里的bean id。你需要在Spring配置里声明:

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

scope="prototype"必须加,因为Struts的Action是有状态的(每个请求一个实例),如果用了默认的singleton,多个请求会共享同一个Action实例,导致数据错乱。这是SSH整合里最隐蔽的坑之一,表现是偶尔登录后看到别人的信息。

4.2 Action中注入Service的两种方式

方式一:XML属性注入。上面已经展示了,在Spring配置里用<property>注入。

方式二:注解注入。在Action类上加@Controller或@Component,在字段上加@Autowired。

@Controller("userAction") @Scope("prototype") public class UserAction extends ActionSupport { @Autowired private UserService userService; private User user; public String login() { User exist = userService.findByUsername(user.getUsername()); if (exist != null && exist.getPassword().equals(user.getPassword())) { ActionContext.getContext().getSession().put("currentUser", exist); return SUCCESS; } addActionError("用户名或密码错误"); return INPUT; } // getter和setter }

注解方式更简洁,但要注意@Scope("prototype")不能漏。另外,如果用了@Controller,Spring的组件扫描要配置对:

<context:component-scan base-package="com.example.action"/>

提示:Struts的Action和Spring MVC的Controller概念不同,不要混淆。Struts Action继承ActionSupport,通过struts.xml映射;Spring MVC Controller用@RequestMapping映射。SSH里用的是前者。

4.3 拦截器栈与参数传递的注意事项

Struts的拦截器栈负责参数封装、验证、文件上传等功能。默认的defaultStack已经包含了大部分常用拦截器。如果你自定义了拦截器,要确保它放在正确的位置。

参数传递方面,Action里的字段名要和表单的name属性一致,Struts会自动封装。比如表单里有<input name="user.username"/>,Action里就要有private User user;和对应的getter/setter。如果User类里没有username字段,或者getter/setter不完整,参数就传不进来。

中文乱码问题在Struts里也常见。除了web.xml里的编码过滤器,还要在struts.xml里加:

<constant name="struts.i18n.encoding" value="UTF-8"/>

这个常量控制Struts内部处理请求和响应的编码。两个都配上,中文问题基本就解决了。

5. 启动验证与常见报错的排查链路

5.1 从启动日志判断容器是否正常初始化

项目部署到Tomcat后,启动日志会打印Spring容器初始化的过程。正常的日志应该能看到:

  • ContextLoaderListener开始初始化
  • 加载applicationContext*.xml文件
  • 创建dataSource、sessionFactory、transactionManager等bean
  • Struts的Dispatcher初始化
  • 没有ERROR级别的日志

如果看到BeanCreationException,说明某个bean创建失败,通常是因为依赖的bean不存在或属性注入失败。日志里会指出是哪个bean、哪个属性,顺着找就行。

如果看到NoSuchMethodError或ClassNotFoundException,基本是依赖冲突,用mvn dependency:tree查看依赖树,找到重复的包,用exclusions排除。

5.2 五个高频报错的根因与修复

报错信息根因修复方案
No Session found for current thread没有配置事务,或事务切点没覆盖到检查<tx:annotation-driven>或AOP切点表达式
Unable to instantiate ActionAction的bean id和struts.xml里的class不一致核对Spring bean id和struts.xml的class属性
org.hibernate.MappingException实体类没有正确扫描或映射检查packagesToScan和实体类的@Entity注解
java.lang.NoClassDefFoundError: javassistjavassist版本冲突在pom里显式声明javassist版本
Struts Problem Report: There is no Action mapped for namespaceURL和struts.xml的namespace或action name不匹配核对访问路径和配置

排查顺序建议:先看日志最上面的Caused by,那是根因;再看Spring容器是否启动成功;最后看Struts的映射是否生效。不要一上来就改代码,先定位问题在哪一层。

5.3 一个最小可运行示例的验证步骤

搭好之后,用一个最简单的登录功能验证全链路:

  1. 建一张user表,插入一条测试数据
  2. 写User实体类,加@Entity和@Table注解
  3. 写UserDao和UserServiceImpl,实现findByUsername
  4. 写UserAction,注入UserService,实现login方法
  5. 配置struts.xml,映射user_login.action
  6. 写login.jsp,表单提交到user_login.action
  7. 部署启动,访问登录页,输入测试账号

如果登录成功跳转到成功页,说明Spring的IoC、Hibernate的ORM、Struts的MVC全链路都通了。如果哪一步失败,按上面的报错表排查。

注意:hbm2ddl.auto=update会自动建表,但字段类型可能和你的预期不一致。建议先手动建表,把hbm2ddl.auto设为validate,让Hibernate只校验不修改。这样表结构可控,也避免开发过程中误改字段。

6. 几个让我印象深刻的坑与绕行方案

第一个坑是Spring配置文件加载顺序。applicationContext-dao.xml里用了${jdbc.url}占位符,但<context:property-placeholder>写在applicationContext.xml里,如果Spring先加载了dao配置再加载总配置,占位符解析不了,启动就报Could not resolve placeholder。解决办法是把property-placeholder放在最早加载的配置文件里,或者用ignore-unresolvable="true"。

第二个坑是Hibernate的懒加载。Service方法里查询了一个关联对象,返回给JSP页面时Session已经关了,访问关联属性就报LazyInitializationException。解决办法是在Service层把需要的数据初始化好,或者用fetch策略改成EAGER,但后者影响性能。我一般用Hibernate.initialize()在Service里手动初始化。

第三个坑是Struts的文件上传。默认的fileUpload拦截器有大小限制,超过就报错。需要在struts.xml里覆盖常量:

<constant name="struts.multipart.maxSize" value="10485760"/>

这个值是10MB,按需调整。同时Action里要用File、String、String三个属性接收上传文件、文件名、内容类型,命名规则是xxx、xxxFileName、xxxContentType。

第四个坑是事务不回滚。默认情况下,Spring事务只对RuntimeException回滚,如果Service方法抛的是Exception(检查型异常),事务不会回滚。需要在@Transactional或<tx:method>里指定rollback-for="Exception"。这个坑在涉及多表操作时特别危险,数据不一致了才发现。

7. 项目跑通之后,还能往哪些方向扩展

一个能登录的SSH项目只是起点。接下来可以加的东西很多:用Struts的拦截器做登录校验,用Spring的AOP做操作日志,用Hibernate的二级缓存提升查询性能,用JFreeChart做数据报表。每加一个功能,都是对这三个框架理解的一次加深。

我个人建议先把权限管理做起来。用拦截器判断Session里有没有用户信息,没有就跳登录页。这个功能不复杂,但能让你把Struts的拦截器机制、Spring的Session管理、Hibernate的用户查询串起来。做完之后,对整个SSH的协作方式会有更直观的感受。

另外,虽然SSH现在不是主流了,但它的分层思想、IoC和AOP的理念、ORM的映射思路,在Spring Boot里一样适用。把SSH搭明白,再去看Spring Boot,会发现很多概念是相通的,只是配置方式从XML变成了注解和自动配置。这个过渡会比直接学Spring Boot更平滑,因为你知道底层发生了什么。

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

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

立即咨询