ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java Web三层架构详解:从餐厅比喻到SSH实战

Java Web三层架构详解:从餐厅比喻到SSH实战

简介:这份文档面向Java Web初学者与需要搭建企业级应用的中级开发者,系统讲解表示层、业务逻辑层与数据访问层的职责划分,并围绕Spring、Struts、Hibernate组成的SSH组合框架,说明如何借助IoC管理Action等bean、以MVC解耦表现与业务逻辑、用ORM简化数据库操作,从而在短期内构建结构清晰、可复用且易维护的Web应用。资源包共1个doc文件,约102KB,内容涵盖三层架构原理、各框架技术要点及基于SSH的Web应用模型设计与实现,并配有案例说明方案在项目中的落地方式。目前已有2378人学习下载,适合希望理解经典SSH整合思路、理清分层设计脉络的读者参考,也可作为课程设计或技术选型时的架构入门材料。

1. 三层架构到底在分什么:从一家餐厅的后厨说起

第一次听到“Java Web 三层架构”,很多人的反应是:是不是要写三个文件夹?是不是要建三个项目?其实都不是。它讲的是一件很朴素的事——把一坨混在一起的代码,按“谁负责什么”切成三块,让改一处不至于牵动全身。

打个比方,一家餐厅能运转,靠的不是一个人从点菜、炒菜到结账全包,而是分成前厅、后厨、采购三拨人:前厅负责接待客人、记下菜单,后厨负责把菜单变成菜,采购负责把食材备齐。前厅不需要知道菜怎么炒,后厨也不需要知道客人坐哪桌。Java Web 三层架构就是这个思路:表示层管“和用户打交道”,业务逻辑层管“规则怎么算”,数据访问层管“数据从哪来、存到哪去”。搞懂它,你再看任何一个企业级 Web 项目的目录结构,就不会一脸懵——这也是为什么“JavaWeb 三层架构”这个词在面试和实际开发里反复出现。

2. 为什么非要分层:耦合这件麻烦事

2.1 不分层会翻车成什么样

想象一个最原始的写法:一个 JSP 页面里,上面写着 HTML 表单,中间嵌着一段 Java 代码判断“这个用户积分够不够兑换”,下面又直接连数据库执行 SQL 把结果写回去。功能能跑,但问题埋得很深。

第一,改界面要动业务代码。产品经理说按钮换个位置,你打开文件发现业务判断和页面标签缠在一起,改一行怕碰坏另一行。第二,换数据库要动全站。原来用某款数据库,后来要换一款,所有写死 SQL 的地方都得翻一遍,这就是典型的硬编码带来的血泪经验。第三,没法并行干活。前端的人和后端的人盯着同一个文件,谁先提交谁后提交都要吵架。

分层要解决的核心就是“耦合”——两个模块之间牵连太紧,动一个另一个就抖。把职责切开之后,表示层只认“我要展示什么数据”,业务层只认“这个规则怎么算”,数据层只认“这张表怎么读写”,中间靠接口和配置连起来,而不是靠代码里互相 new 出来。

2.2 三层各自守的是什么门

表示层是门面。它接收用户请求、组织页面展示,本身不写业务规则。在经典的 SSH 组合里,这一层由 Struts 承担,它基于 MVC 模式,把视图(JSP 页面)、控制器(ActionServlet 加 Action)、模型拆开。用户点一个链接发出请求,ActionServlet 按配置文件把请求转给对应的 Action,Action 再去喊业务层干活,拿到结果后跳转到指定页面。页面里只有标记,没有流程逻辑,这是它和“JSP 里写满 Java”最大的区别。

业务逻辑层是大脑。积分怎么算、权限怎么判、一次下单要扣几次库存,这些规则都归它。Spring 在这一层的作用是把各个组件“装配”起来,靠的是 IoC,也就是控制反转。通俗地说,以前是代码自己 new 出一个对象来用,现在是容器提前把对象建好、把依赖塞进去,代码只管声明“我需要谁”。依赖注入让替换实现变得容易,测试时塞一个假的进去就行。

数据访问层是仓库管理员。它只负责和数据库打交道,把 Java 对象和数据库表对应起来。Hibernate 做的是 O/R mapping,也就是对象关系映射,让程序员用操作对象的方式操作数据,不用手写大量 JDBC 代码。换数据库时,往往只改初始化配置文件,应用代码基本不动。

提示:三层是“逻辑分层”,不是“物理分机器”。三层可以跑在同一台服务器上,也可以拆开部署,分层讲的是职责边界,不是部署位置。

2.3 分层的代价也要说清楚

分层不是白拿的好处。层多了,一次请求要穿过表示层、业务层、数据层,调用链变长,排查问题时要在几个文件之间跳。小项目、原型、一次性脚本,硬套三层反而累赘。所以判断标准不是“三层一定好”,而是“这个项目会不会长期维护、会不会多人协作、会不会换数据库或换前端”。会,就值得分;不会,就别为了架构而架构。

3. 三层怎么落到代码里:SSH 组合的真实分工

3.1 一次请求的完整旅程

拿一个用户管理模块举例,看一次“列出所有用户”的请求是怎么走的。用户在页面点击,发出ListUser.do请求;Struts 的 ActionServlet 按struts-config.xml的配置,把这个请求交给对应的 Action;Action 调用业务层的用户服务类;服务类再调用数据访问层的用户 DAO;DAO 通过 Hibernate 去查数据库,把结果一层层返回,最后 Action 把数据放进请求作用域,跳转到展示页面。

这条链路里,每一层只和相邻层说话。Action 不认识数据库,DAO 不认识页面,中间靠接口隔开。这就是“高内聚、低耦合”落到实处的样子。

3.2 数据访问层:DAO 为什么常写成接口加实现

数据访问层通常由持久化类和 DAO 组成,每张表对应一个持久化对象,这样就能用面向对象的方式思考数据。DAO 负责具体的增删改查。这里有个初学者常踩的坑:把 DAO 直接写成一个能操作的类,而不是先定接口再写实现。

// 先定义接口,声明“能做什么” public interface UserDao { int add(User user); List findAll(); } // 再写实现,放进单独的 impl 包 public class UserDaoImpl extends HibernateDaoSupport implements UserDao { public int add(User user) { // 借助 Spring 提供的 HibernateTemplate 完成保存 return Integer.parseInt(this.getHibernateTemplate().save(user).toString()); } public List findAll() { return this.getHibernateTemplate().loadAll(User.class); } }

接口和实现分开,好处是业务层只依赖接口,Spring 可以在运行时决定注入哪个实现,测试时也能换成假实现。继承HibernateDaoSupport是为了直接拿到HibernateTemplate,省去自己管理 Session 的麻烦。数据源、Session 工厂、事务管理这些,交给 Spring 容器统一配置,DAO 里不用重复写。

3.3 业务逻辑层:IoC 到底翻转了什么

业务层的服务类负责把业务规则写清楚,它调用 DAO 拿数据,算完再返回。关键在于这些组件不是自己 new 出来的,而是由 Spring 容器管理。传统写法里,服务类要用手工方式创建 DAO,还得自己处理单例、工厂这些模式;用了 IoC 之后,服务类只声明依赖,容器负责注入。

public class UserServiceImpl implements UserService { // 依赖由 Spring 注入,而不是在这里 new private UserDao userDao; public void setUserDao(UserDao userDao) { this.userDao = userDao; } public List listAllUsers() { // 业务层可以在这里加权限判断、数据加工等规则 return userDao.findAll(); } }

setUserDao就是设值注入的入口,配置文件里把某个 DAO 实现绑到这个属性上,容器启动时就完成了装配。这样业务层不关心 DAO 怎么实现,只关心它能不能干活。事务、连接池这些横切的服务,也由容器统一提供,业务代码里看不到它们,却实实在在享受着。

3.4 表示层:Action 只做调度,不做业务

表示层的 Action 职责很轻:接请求、调业务、选页面。它不应该在里面写“积分怎么算”这种规则,否则业务逻辑又漏回表示层了。Struts 的配置文件负责把请求路径映射到 Action,Action 处理完返回一个结果标识,框架据此跳转到对应页面。页面用标签库展示数据,尽量不写 Java 脚本片段。

<!-- struts-config.xml 片段:把请求路径和 Action 绑起来 --> <action path="/listUser" type="com.example.web.ListUserAction"> <forward name="success" path="/userList.jsp"/> </action>

这段配置的意思是:访问/listUser时交给指定的 Action 处理,处理成功跳到用户列表页。路径、类名、跳转目标都写在配置里,改流程不用改 Java 代码,这也是 MVC 分离带来的便利。

4. 容易混淆的几个概念:别把分层和分包搞混

4.1 三层架构和 MVC 是一回事吗

易混点:很多人把三层架构和 MVC 当成同义词,面试时张口就说“三层就是 MVC”。为什么混:两者都讲“分开”,而且表示层里确实用了 MVC。怎么区分:MVC 是一种界面层的组织方式,管的是视图、控制器、模型在“展示”这件事上怎么分工;三层架构管的是整个应用纵向切成表示、业务、数据三块。MVC 通常落在表示层内部,是三层里的一层再细分,两者是不同粒度的东西。

4.2 三层和“三个包”是不是对应关系

易混点:以为建了 controller、service、dao 三个包就等于三层架构。为什么混:目录结构看起来确实像。怎么区分:分层看的是依赖方向和职责边界,不是文件夹名字。如果 controller 包里直接写了 SQL,或者 dao 包里塞了业务判断,那即使包名齐全,分层也已经破了。包是形式,职责才是实质。

4.3 DAO 和 VO、DTO 分不清

易混点:把 DAO、VO、DTO 都当成“装数据的类”。为什么混:名字都带 O,都和数据有关。怎么区分:DAO 是“访问数据的对象”,负责和数据库交互,有方法有逻辑;VO 是值对象,DTO 是数据传输对象,主要用来在层与层之间搬数据,原则上以承载字段为主。VO 和 DTO 不负责存取,DAO 不负责跨层传值,职责完全不同。

4.4 分层越多越好吗

易混点:觉得层数越多越“架构”。为什么混:大项目里确实能看到更多层,比如域模型层、防腐层。怎么区分:层是为解决问题存在的,每多一层就多一次调用、多一处维护。判断依据是“这一层有没有独立的、会变化的责任”,没有就别加。三层是常见起点,不是必须上限,也不是必须下限。

5. 什么时候该用三层:一个判断清单和我的习惯

真正要落地时,别急着画架构图,先问几个问题。项目会不会长期迭代?会不会有几个人同时改不同模块?数据库或前端有没有可能更换?业务规则会不会越来越复杂?如果答案多是“会”,三层架构就值得上;如果只是一个内部小工具、几天就交付、以后基本不动,硬拆三层反而增加负担,一个文件写完更省事。

具体到 SSH 这套组合,它的适用面也有边界。Struts 负责表示层的 MVC 分离,Spring 负责业务层的装配和事务,Hibernate 负责数据层的对象映射,三者各管一段,适合结构清晰、需要协作的企业级 Web 应用。但它不是万能药:如果团队对这套组合不熟,配置文件的维护成本会变成新的坑;如果业务极简,引入三个框架的复杂度可能超过收益。选型看的是团队和项目,不是看哪个名字更响。

场景建议原因
多人协作、长期维护的企业应用采用三层,职责写清改一层不牵动其他层,便于并行
短期原型、一次性脚本不必强套三层分层成本高于收益
需要频繁换数据库数据层用 ORM 隔离换库主要改配置,少动业务代码
业务规则简单、页面为主表示层可简化过度分层会让调用链变长

有个习惯我后来一直保持:每次新建模块,先把“这段代码属于哪一层”写在一张纸上,再动手建包。曾经有一次图省事,在 Action 里直接调了数据库,当时跑得挺顺,后来要加一层权限校验,发现规则散落在好几个 Action 里,只能一个个补,那种后悔药没处买。从那以后我每次建模块都强制走一遍“先定层、再定包、最后写实现”的顺序,宁可开头多花十分钟。

注意:分层是为了让变化可控,不是为了好看。判断一个分层是否成立,看的是“改一处时,需要动几个文件”,而不是“目录有几个”。

希望这套讲法能帮你在下次看到三层架构时,不再只记住三个名词,而是知道每一层为什么存在、边界在哪、什么时候该用、什么时候该收手。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进