SSH框架整合实战:从零搭建可运行的Java Web项目 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.version4.3.30.RELEASE/spring.version struts.version2.3.37/struts.version hibernate.version4.3.11.Final/hibernate.version /properties dependencies !-- Spring核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-orm/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-tx/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-web/artifactId version${spring.version}/version /dependency !-- Struts2与Spring整合 -- dependency groupIdorg.apache.struts/groupId artifactIdstruts2-core/artifactId version${struts.version}/version /dependency dependency groupIdorg.apache.struts/groupId artifactIdstruts2-spring-plugin/artifactId version${struts.version}/version /dependency !-- Hibernate核心 -- dependency groupIdorg.hibernate/groupId artifactIdhibernate-core/artifactId version${hibernate.version}/version /dependency dependency groupIdorg.hibernate/groupId artifactIdhibernate-entitymanager/artifactId version${hibernate.version}/version /dependency !-- 数据库驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency !-- 连接池 -- dependency groupIdcom.mchange/groupId artifactIdc3p0/artifactId version0.9.5.5/version /dependency !-- 日志 -- dependency groupIdorg.slf4j/groupId artifactIdslf4j-log4j12/artifactId version1.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-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext*.xml/param-value /context-param !-- Struts2核心过滤器 -- filter filter-namestruts2/filter-name filter-classorg.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter/filter-class /filter filter-mapping filter-namestruts2/filter-name url-pattern/*/url-pattern /filter-mapping !-- 字符编码过滤器必须放在Struts之前 -- filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/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和closeSpring会通过HibernateTemplate或直接注入SessionFactory来操作。bean iddataSource classcom.mchange.v2.c3p0.ComboPooledDataSource property namedriverClass value${jdbc.driver}/ property namejdbcUrl value${jdbc.url}/ property nameuser value${jdbc.username}/ property namepassword value${jdbc.password}/ property namemaxPoolSize value20/ property nameminPoolSize value5/ /bean bean idsessionFactory classorg.springframework.orm.hibernate4.LocalSessionFactoryBean property namedataSource refdataSource/ property namepackagesToScan valuecom.example.entity/ 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 /beanpackagesToScan是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 locationclasspath: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 idtransactionManager classorg.springframework.orm.hibernate4.HibernateTransactionManager property namesessionFactory refsessionFactory/ /bean !-- 注解驱动事务 -- tx:annotation-driven transaction-managertransactionManager/ !-- 或者用AOP切面配置 -- tx:advice idtxAdvice transaction-managertransactionManager tx:attributes tx:method namesave* propagationREQUIRED/ tx:method nameupdate* propagationREQUIRED/ tx:method namedelete* propagationREQUIRED/ tx:method namefind* read-onlytrue/ tx:method name* propagationREQUIRED/ /tx:attributes /tx:advice aop:config aop:pointcut idservicePointcut expressionexecution(* com.example.service..*.*(..))/ aop:advisor advice-reftxAdvice pointcut-refservicePointcut/ /aop:config用AOP切面配置的好处是不用在每个Service类上加Transactional注解按方法名前缀自动匹配事务策略。find*方法设为read-onlytrue可以优化性能Hibernate会跳过脏检查。save*、update*、delete*用REQUIRED传播行为表示如果当前没有事务就新建一个有就加入。这里有个细节切点表达式execution(* com.example.service..*.*(..))里的..表示匹配子包如果你把Service实现类放在service.impl下这个表达式能覆盖到。但如果写成了com.example.service.*.*(..)就只匹配service包下的类不包含子包事务会失效。这个坑我踩过排查了半天才发现是切点写窄了。4. Struts与Spring的整合Action里怎么注入Service4.1 struts.xml的核心配置与通配符映射Struts的配置文件负责URL到Action的映射。用通配符可以少写很多重复配置。struts constant namestruts.objectFactory valuespring/ constant namestruts.devMode valuetrue/ package namedefault extendsstruts-default namespace/ action nameuser_* classuserAction method{1} result namesuccess/WEB-INF/jsp/user/{1}.jsp/result result nameinput/WEB-INF/jsp/user/{1}.jsp/result /action /package /strutsstruts.objectFactory设为spring是关键它告诉Struts用Spring的容器来创建Action实例这样Action里才能注入Service。user_*这个通配符映射表示访问user_login.action会调用userAction的login方法成功后跳转到/WEB-INF/jsp/user/login.jsp。一个配置覆盖了增删改查所有操作。classuserAction这里的userAction不是类全名而是Spring容器里的bean id。你需要在Spring配置里声明bean iduserAction classcom.example.action.UserAction scopeprototype property nameuserService refuserService/ /beanscopeprototype必须加因为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)不能漏。另外如果用了ControllerSpring的组件扫描要配置对context:component-scan base-packagecom.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 nameuser.username/Action里就要有private User user;和对应的getter/setter。如果User类里没有username字段或者getter/setter不完整参数就传不进来。中文乱码问题在Struts里也常见。除了web.xml里的编码过滤器还要在struts.xml里加constant namestruts.i18n.encoding valueUTF-8/这个常量控制Struts内部处理请求和响应的编码。两个都配上中文问题基本就解决了。5. 启动验证与常见报错的排查链路5.1 从启动日志判断容器是否正常初始化项目部署到Tomcat后启动日志会打印Spring容器初始化的过程。正常的日志应该能看到ContextLoaderListener开始初始化加载applicationContext*.xml文件创建dataSource、sessionFactory、transactionManager等beanStruts的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 一个最小可运行示例的验证步骤搭好之后用一个最简单的登录功能验证全链路建一张user表插入一条测试数据写User实体类加Entity和Table注解写UserDao和UserServiceImpl实现findByUsername写UserAction注入UserService实现login方法配置struts.xml映射user_login.action写login.jsp表单提交到user_login.action部署启动访问登录页输入测试账号如果登录成功跳转到成功页说明Spring的IoC、Hibernate的ORM、Struts的MVC全链路都通了。如果哪一步失败按上面的报错表排查。注意hbm2ddl.autoupdate会自动建表但字段类型可能和你的预期不一致。建议先手动建表把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-unresolvabletrue。第二个坑是Hibernate的懒加载。Service方法里查询了一个关联对象返回给JSP页面时Session已经关了访问关联属性就报LazyInitializationException。解决办法是在Service层把需要的数据初始化好或者用fetch策略改成EAGER但后者影响性能。我一般用Hibernate.initialize()在Service里手动初始化。第三个坑是Struts的文件上传。默认的fileUpload拦截器有大小限制超过就报错。需要在struts.xml里覆盖常量constant namestruts.multipart.maxSize value10485760/这个值是10MB按需调整。同时Action里要用File、String、String三个属性接收上传文件、文件名、内容类型命名规则是xxx、xxxFileName、xxxContentType。第四个坑是事务不回滚。默认情况下Spring事务只对RuntimeException回滚如果Service方法抛的是Exception检查型异常事务不会回滚。需要在Transactional或tx:method里指定rollback-forException。这个坑在涉及多表操作时特别危险数据不一致了才发现。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更平滑因为你知道底层发生了什么。