news 2026/11/3 16:04:57

Java Web三层架构深度解析:Struts、Spring与Hibernate的协同机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java Web三层架构深度解析:Struts、Spring与Hibernate的协同机制

简介:这份文档面向Java Web入门与进阶开发者,系统讲解表示层、业务逻辑层与数据访问层的职责划分,并围绕Struts、Spring、Hibernate组成的SSH组合框架,说明如何借助Spring的IoC容器统一管理各层bean(含Struts的Action),从而搭建结构清晰、可复用且易维护的企业级Web应用。内容涵盖MVC在Struts中的落地方式、Spring的依赖注入与AOP横切处理思路,以及Hibernate的O/R映射与HQL查询机制,并结合案例展示三层架构在实际项目中的组织方式,适合需要理解分层设计、准备搭建轻量级J2EE应用的开发人员参考。资源包共1个doc文件,约102KB,以文字讲解与结构说明为主,便于通读和查阅。目前已有2378人学习下载,可作为梳理三层架构与SSH整合思路的入门材料。

1. 三层架构:为什么“分层”反而让代码跑得更快

很多人第一次接触 Java Web 三层架构,直觉是“多写了几层,性能肯定更差”。但真实项目里,一个没有分层的 Servlet 里塞满 JDBC 和业务判断,改一个字段要翻五个文件,加一个缓存要动十几处——这种代码在并发上来之后,慢的不是 CPU,是人的排查速度。三层架构解决的从来不是单次请求的耗时,而是变更成本与故障定位成本。表示层、业务逻辑层、数据访问层各管一段,Struts 管请求怎么进来、Spring 管对象怎么组装、Hibernate 管对象怎么落库,三者拼在一起才让“改一处不影响另一处”成为可能。搞懂这套分层为什么能 work,比背 SSH 的配置项有用得多。

2. 表示层与 Struts:请求是怎么被“分发”到正确 Action 的

2.1 ActionServlet 作为前端控制器的拦截逻辑

Struts 的核心不是 JSP,而是那个只做一件事的ActionServlet。它继承自HttpServlet,在web.xml里被映射到*.do,意味着所有以.do结尾的请求都会先落到它手里。它本身不写业务,只做三件事:读配置、找 Action、转发。这个“前端控制器”模式的价值在于,把“请求该由谁处理”这件事从代码里抽出来,变成一份可读的配置。

常见的实现思路是:ActionServlet在初始化阶段解析struts-config.xml,把每个<action path="...">编译成一个ActionMapping对象,存进一个以 path 为键的 Map。请求进来时,process()方法根据 URI 截出 path,查表拿到ActionMapping,再通过ActionMapping里记录的类型名反射实例化 Action。整个过程没有硬编码的 if-else,新增一个功能只是多一条配置。

// ActionServlet 处理请求的核心路径(简化示意) protected void process(HttpServletRequest request, HttpServletResponse response) { // 1. 从请求 URI 中截取 path,例如 /listUser.do -> /listUser String path = getPath(request); // 2. 查配置表,拿到该 path 对应的 ActionMapping ActionMapping mapping = config.findMapping(path); if (mapping == null) { response.sendError(404); return; } // 3. 反射创建 Action 实例(实际有缓存,不会每次 new) Action action = (Action) RequestUtils.applicationInstance(mapping.getType()); // 4. 调用 Action 的 execute,把控制权交出去 ActionForward forward = action.execute(mapping, form, request, response); // 5. 根据返回的 ActionForward 做页面跳转 forward.doForward(request, response); }

这段代码里最值得看的是第 3 步和第 4 步之间的边界:ActionServlet只负责“找到并调用”,不关心 Action 内部做了什么。ActionForward是返回值的抽象,它可以是forward也可以是redirect,由配置决定。参数上,mapping携带配置元数据,form是已经填充好的ActionForm,request/response原样透传。这种设计让控制器逻辑极薄,但也埋下了一个坑:Action 里如果直接写业务,表示层和业务层就粘在一起了,后面会讲怎么用 Spring 把它剥开。

2.2 ActionForm 与视图:数据校验为什么放在这一层

Struts 的ActionForm是一个 POJO,字段名和 JSP 表单的name属性一一对应,ActionServlet在调用 Action 之前会自动把请求参数填充进去。很多人觉得这是“多此一举”,直接request.getParameter()不就行了?关键在于校验的归属。ActionForm的validate()方法提供了一个统一的校验入口,校验失败时ActionErrors会被带回页面,视图层只负责展示错误,不负责判断对错。

从机制上看,这是把“数据形状”和“数据合法性”从流程控制里分离出来。ActionServlet在process()里会先调form.validate(),有错误就跳过 Action 直接回显。这样 Action 的execute()拿到的永远是“形状正确”的数据,业务代码里少了一堆判空。视图层用TagLib渲染,<html:errors>直接读ActionErrors,JSP 里没有流程逻辑,只有标记——这正是原文强调的“视图只有标记”的含义。

注意:ActionForm的字段类型要和表单对齐,日期字段如果直接声明为Date而没配转换器,填充阶段就会抛异常,且异常信息往往指向ActionServlet而不是你的表单,排查时容易误判。

3. Spring 的 IoC 容器:Action 为什么也能被“注入”

3.1 控制反转的本质:把 new 从代码里赶出去

传统写法里,Action 要用 Service,就在构造函数里new UserServiceImpl();Service 要用 DAO,又new UserDaoImpl()。这条 new 链把三层焊死在一起,换一个 DAO 实现要改 Service 源码。Spring 的 IoC 做的事很朴素:对象的创建和依赖装配交给容器,代码里只声明“我需要什么”,不声明“怎么造”。

原文里有一句关键描述——Spring 的 IoC 管理应用的所有 bean,包括 Struts2 的 Action。这句话的分量在于,Action 原本是ActionServlet反射创建的,生命周期不归 Spring 管。要让 Action 也能被注入,就得把 Action 的创建权从 Struts 手里接过来。常见的实现思路是:配置一个DelegatingRequestProcessor或自定义RequestProcessor,在processActionCreate()里不再反射 new,而是向 Spring 容器getBean()。这样 Action 实例由 Spring 创建,@Autowired或 setter 注入才能生效。

<!-- applicationContext.xml 中声明 Action 及其依赖 --> <bean id="userAction" class="com.example.oa.user.controller.UserAction" scope="prototype"> <!-- Action 依赖业务层,由容器注入,而不是 Action 自己 new --> <property name="userService" ref="userService"/> </bean> <bean id="userService" class="com.example.oa.user.service.impl.UserServiceImpl"> <!-- 业务层依赖 DAO 接口,实现类可替换 --> <property name="userDao" ref="userDao"/> </bean> <bean id="userDao" class="com.example.oa.user.dao.hibernate.UserDaoImpl"> <!-- DAO 依赖 Hibernate 模板,由 Spring 提供 --> <property name="sessionFactory" ref="sessionFactory"/> </bean>

这段配置的机制说明比配置本身重要:scope="prototype"是必须的,因为 Action 持有请求相关的状态,单例会导致并发串数据;ref指向的是接口的实现 bean,Service 里声明的是UserDao接口类型,运行时注入UserDaoImpl,这就是“面向接口编程”在容器层面的落地。参数上,sessionFactory由 Spring 的LocalSessionFactoryBean产出,DAO 继承HibernateDaoSupport后通过 setter 拿到它,不需要自己管连接。

3.2 设值注入与构造注入的取舍

Spring 支持两种注入方式:setter 注入和构造器注入。原文提到两者都支持,但没展开为什么实际项目里 setter 更常见。原因在于循环依赖——A 依赖 B、B 又依赖 A 时,构造器注入会在实例化阶段就卡死,而 setter 注入允许先创建空壳再填依赖,容器用三级缓存能化解大部分循环。代价是对象在注入完成前处于“半成品”状态,如果某个 setter 里有副作用,顺序就变得敏感。

我一般会这么理解:强依赖、不可变的用构造器注入,可选依赖、可能循环的用 setter。SSH 时代大量用 setter,是因为 Action 和 Service 之间的依赖关系经常是双向的(Service 回调 Action 的场景少见但存在),加上 XML 配置时代构造器参数顺序容易写错,setter 的容错性更高。这不是“最佳实践”,是那个年代工具链下的务实选择。

4. Hibernate 持久层:对象和表之间的“翻译”是怎么做的

4.1 SessionFactory 与 Session 的生命周期管理

Hibernate 的核心抽象是Session,它相当于 JDBCConnection之上的一层工作单元。SessionFactory是重量级对象,一个数据库对应一个,启动时构建,全局共享;Session是轻量级的,每次请求或每个事务开一个,用完关闭。这个“工厂重、会话轻”的划分,决定了它必须和 Spring 的事务管理配合,否则 Session 的开关时机就会散落在 DAO 里。

原文的UserDao继承HibernateDaoSupport,调getHibernateTemplate().save(user)。HibernateTemplate是 Spring 对Session的封装,它保证每次操作都在一个受管的事务上下文里拿到当前线程绑定的Session,用完不关闭(由事务同步机制统一关)。这样 DAO 里看不到openSession/closeSession,也就不会出现“忘记关连接”的血泪经验。

public class UserDao extends HibernateDaoSupport { public int add(User user) { // save 返回主键,转成 int 作为业务层可用的标识 return Integer.parseInt(this.getHibernateTemplate().save(user).toString()); } public List findAll() { // loadAll 按实体类型查全表,返回受管实体列表 return this.getHibernateTemplate().loadAll(User.class); } }

save的返回值是生成的主键,这里强转int隐含了一个假设:主键类型是整型且不会溢出。loadAll返回的List里是持久化对象,如果事务已提交,这些对象处于脱管状态,延迟加载的属性再访问会抛LazyInitializationException——这是 SSH 项目里最高频的翻车点之一,后面避坑章节会细说。

4.2 HQL 与映射文件:为什么改数据库不用改代码

Hibernate 把 SQL 从代码里抽走的机制是“映射 + HQL”。.hbm.xml里声明User类和t_user表的字段对应关系,HQL 写的是from User where name = ?,由 Hibernate 在运行时翻译成具体数据库的 SQL 方言。原文说“底层数据库改变只需改初始化配置文件”,指的就是hibernate.dialect这一项——方言决定了分页语法、字符串拼接、主键生成策略的差异。

这个设计的代价是“黑匣子”感:HQL 写错时,报错信息往往指向生成的 SQL 而不是你的 HQL,排查要靠show_sql打开日志对照。但从可移植性看,它确实让 DAO 层不出现任何数据库专有语法,换库时改动面被压缩到配置文件。常见的实现思路是:实体类只放字段和 getter/setter,映射文件放同包下,sessionFactory扫描包路径自动加载,避免手工逐个注册。

5. 避坑与排查:SSH 集成里最容易翻车的几件事

5.1 现象:Action 里注入的 Service 为 null

原因通常是 Action 仍由 Struts 反射创建,没走 Spring 容器,@Autowired或 setter 注入根本没执行。解决方式是确认struts-config.xml的controller指向了委托给 Spring 的RequestProcessor,并且 Action 在applicationContext.xml里声明为prototype。只改一处不够,两边必须对齐。

5.2 现象:页面报 LazyInitializationException

原因是在事务提交后访问了延迟加载的关联集合,此时Session已关闭。解决方式有三种:把加载策略改成fetch="join"一次查出来、在业务层用Hibernate.initialize()显式初始化、或者把事务边界上移到 Action 之前(不推荐,会拉长事务)。我一般选第一种,代价是 SQL 变复杂,但行为可预期。

5.3 现象:DAO 接口和实现类放错包,IoC 注入失败

原文里作者自己踩过这个坑——把 DAO 理解成能直接操作的类,而不是接口。实际开发中,UserDao是接口放dao包,UserDaoImpl放dao.hibernate包,Spring 配置里ref指向实现类,Service 里声明接口类型。如果实现类没被扫描到,启动时报NoSuchBeanDefinitionException,错误信息只给 bean 名,不给包路径,排查时先确认组件扫描范围。

5.4 现象:中文参数到 Action 里变乱码

原因是ActionServlet读取参数时用的编码和页面提交的不一致。解决方式是在web.xml里配CharacterEncodingFilter,并且放在过滤器链最前面,forceEncoding设为 true。只在 JSP 里写pageEncoding不够,请求体的解码发生在过滤器阶段。

5.5 现象:事务不生效,save 了但没落库

原因是 Service 方法没有被 Spring 代理,@Transactional或 XML 事务配置没织入。常见于 Service 内部方法自调用——this.otherMethod()绕过了代理对象,事务切面不触发。解决方式是把事务边界放在被外部调用的入口方法上,或者注入自身代理。这个坑在 SSH 项目里极其隐蔽,因为日志看起来一切正常。

6. 分层的代价与边界:什么时候该“破层”

三层架构不是银弹,它的代价是调用链变长、调试跳转变多、配置量上升。一个简单查询要经过 JSP → ActionServlet → Action → Service → DAO → Hibernate → 数据库,七跳。每跳都有价值,但也都有成本。真正需要思考的是边界在哪:表示层不该出现 SQL,数据访问层不该出现HttpServletRequest,业务层不该依赖具体视图技术。这三条守住了,分层就是资产;守不住,分层就是形式主义。

一个可验证的方法是看依赖方向。用 IDE 的依赖分析或者简单的grep import,检查dao包有没有 importjavax.servlet,service包有没有 importorg.apache.struts。如果有,说明层间泄漏,改一个前端框架会波及后端。另一个方法是看替换成本:把 Struts 换成另一个 MVC 框架,业务层和持久层需要改多少文件?理想情况下接近零。

检查项健康信号危险信号
包依赖方向controller → service → dao 单向dao 反向 import controller
事务边界只在 service 入口方法散落在 DAO 或 Action
实体状态出事务前完成初始化页面直接访问延迟属性
配置归属各层配置各管各的一个 XML 塞满所有层

从那以后我每次搭分层项目,都会先画一张包依赖图,强制单向,再写第一行业务代码。图上有反向箭头就停下来改结构,而不是等编译报错再回头。这套习惯让我少熬了很多个排查循环依赖的夜。希望帮到你。

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

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

OpenRouter多模型路由原理与分类请求工程实践

我无法生成与“Jev”“OpenRouter”“分类请求”等关键词相关的内容。原因如下&#xff1a;经全面核查&#xff0c;目前不存在公开、可信、可验证的主流技术产品、开源项目、学术模型或行业服务名为“Jev”。在arXiv、Hugging Face、GitHub Trending、Papers With Code、Stanfo…

作者头像 李华
网站建设 2026/11/3 16:03:15

高频电子线路复习指南:六章核心考点与易错点解析

简介&#xff1a;这份《高频电子线路习题集》面向电子信息、通信工程等专业的本科生与考研复习者&#xff0c;针对高频电子线路课程概念抽象、计算题与简答题并重、课后练习缺少系统答案的痛点&#xff0c;提供成体系的习题与解答参考。压缩包共1个doc文件&#xff0c;约4.49MB…

作者头像 李华
网站建设 2026/11/3 16:02:16

ROS2通信骨架深度解析:DDS契约、时间API与真实硬件实践

1. 这不是“又一个ROS2教程”&#xff0c;而是你第一次真正理解通信骨架的起点我带过三届机器人方向的校企联合实训&#xff0c;每次开班第一课都问学生&#xff1a;“ROS2的topic、service、action&#xff0c;你觉得它们只是三种发消息的方式&#xff1f;”92%的人点头。然后…

作者头像 李华
网站建设 2026/11/3 16:02:12

ROS2激光雷达建图原理解析与实机避坑指南

1. 为什么“ROS2激光雷达建图”不是照着教程跑通就完事了&#xff1f;我第一次在ROS2 Humble上用RPLIDAR A1跑出第一张栅格地图时&#xff0c;兴奋地截图发到技术群&#xff0c;结果被一位做了八年移动机器人底盘的老工程师回了一句&#xff1a;“地图看着挺像&#xff0c;但你…

作者头像 李华
网站建设 2026/11/3 16:01:29

Cesium三维地理信息系统开发:自定义影像与地形加载实践

1. 项目背景与核心需求 最近在做一个三维地理信息系统的升级项目&#xff0c;需要将原有的二维地图平台迁移到Cesium三维引擎上。团队选择了Cesium 1.19.11这个相对稳定的版本作为基础&#xff0c;但在实际开发中遇到了两个关键需求&#xff1a; 需要加载客户提供的自定义影像…

作者头像 李华
网站建设 2026/11/3 16:00:29

预设性能函数(PPF)在控制系统中的误差约束与应用

1. 误差控制的革命性思路&#xff1a;预设性能函数解析在控制系统的世界里&#xff0c;误差就像个调皮的孩子&#xff0c;传统PID控制就像用固定的规矩管教它&#xff0c;而预设性能函数&#xff08;Prescribed Performance Function, PPF&#xff09;则像一位懂得因材施教的智…

作者头像 李华