简介:本资源是一份面向Java Web初学者与中级开发者的三层架构系统性学习文档,聚焦企业级Web应用的结构设计与SSH框架整合实践。内容深入剖析表示层(Struts实现MVC解耦)、业务逻辑层(Spring IoC/DI与AOP管理)及数据访问层(Hibernate ORM映射),结合Spring 2.5、Struts 2.0与Hibernate 3.2的具体集成方案,提供从理论模型到案例落地的完整技术路径。资源为单文件Word文档(.doc),共1个文件,大小102KB,轻量易读,适合作为架构入门笔记、课堂讲义或项目设计参考。目前已有2376人学习下载,文档涵盖SSH框架原理对比、各层组件协作机制、配置要点(如struts-config.xml、hibernate.cfg.xml)、IoC容器管理Action等关键细节,并附有典型应用场景说明与技术选型分析,帮助读者建立清晰的分层思维与工程化开发认知。
1. Java Web 三层架构到底是什么:就像开一家连锁奶茶店,每层干好自己的活,谁也不越界
你有没有见过那种“一锅乱炖”的Java Web项目?所有代码挤在同一个类里:用户点个登录按钮,页面跳转、密码校验、数据库查表、返回结果、错误提示……全塞进一个Servlet里。改个密码规则要翻十页代码,换数据库得重写三成逻辑,加个新功能怕牵一发而动全身——这不是开发,是拆弹。
Java Web 三层架构,就是给这种混乱局面装上“岗位说明书”:它把一个Web应用像一家标准化连锁奶茶店那样,明确划分为表示层(前台)、业务逻辑层(店长+配方师)、数据访问层(后厨+仓库)。前台只管接单、出杯、微笑;店长不碰奶盖机,只管审核订单、计算积分、处理会员升级;后厨不和顾客说话,只按标准配方打料、从冷库取原料、记录库存消耗。三层之间用“标准单据”(接口)交接,谁生病了、换人了、升级设备了,其他两层几乎不用改。
这不是炫技,而是大型项目能活下去的底线。当团队从3人扩到30人,当系统从“用户管理”变成“千人并发抢券+实时库存扣减+风控拦截+多渠道对账”,没有清晰分层,就像让奶茶店员既当收银又炒茶又修冰箱——不是累倒,就是翻车。今天讲的这份《Java Web 三层架构详解》,正是当年某高校实验室为某通信公司问答系统落地时沉淀的实战手册,它不讲虚概念,只拆解SSH(Spring + Struts + Hibernate)如何把这三层真正“焊死”又“松绑”:Struts怎么当好前台调度员,Spring怎么当好店长兼人事总监,Hibernate怎么当好后厨智能配货系统。你看完会明白:所谓架构,不是画在PPT上的漂亮方块,而是让每个程序员只盯着自己那块操作台,却能稳稳端出整杯奶茶的协作契约。
2. 表示层:Struts 不是万能胶,而是精准的“请求交通指挥中心”
Struts 常被误认为“就是写JSP和Action”,但它的核心价值远不止于此。它本质是一个基于MVC模式的请求路由与流程控制器,目标很朴素:把“用户点什么”和“该找谁干活”这两件事,彻底分开。
2.1 它到底管什么?三件事,缺一不可
- 统一入口守门员:所有HTTP请求(比如
/user/list.do、/order/submit.do)必须先撞上ActionServlet—— 这是Struts的唯一门面。它不处理业务,只做两件事:解析URL后缀(.do),然后查配置表(struts-config.xml):“这个请求,该派给哪个Action?” - 流程编排导演:Action本身不是业务员,而是“工单派发员”。它拿到请求参数后,调用Spring管理的Service对象(比如
userService.findAll()),拿到结果后,决定下一步去哪——是跳转到success.jsp,还是重定向到error.do,甚至转发给另一个Action。整个过程像快递分拣线:扫码→查路由表→贴新单号→推入对应滑槽。 - 视图减负助手:Struts自带的标签库(
<s:textfield>、<s:iterator>)不是为了炫技,而是把JSP从“写Java代码”中解放出来。以前JSP里满屏<% if(user!=null){ %>,现在变成<s:if test="user != null">—— 标签背后是OGNL表达式引擎,它让页面只专注“怎么展示”,不掺和“怎么判断”。
提示:Struts 2.x 的
Action类默认是无状态的(每次请求新建实例),所以别在Action里存用户数据或缓存结果。那是Service层的事。很多初学者在这里踩坑:把查询结果存在Action字段里,结果下个请求发现数据没了——不是Bug,是设计使然。
2.2 配置即契约:struts-config.xml 是你的交通法规
这份XML文件,就是Struts世界的宪法。它定义了“谁响应什么请求”、“响应后去哪”、“出错怎么办”。看一个真实片段:
<!-- struts-config.xml 片段 --> <action-mappings> <!-- 定义一个动作:当请求 /user/list.do 时 --> <action path="/user/list" type="com.simon.oa.user.controler.UserListAction" name="userForm" scope="request" validate="true"> <!-- 成功后跳转到 list.jsp --> <forward name="success" path="/user/list.jsp"/> <!-- 失败后跳转到 error.jsp --> <forward name="error" path="/error.jsp"/> </action> </action-mappings>这里的关键参数:
path="/user/list":这是URL路径(去掉.do后缀),用户浏览器地址栏看到的就是这个。type="...UserListAction":这是具体干活的Action类全名。注意!它不是由new出来的,而是由Spring容器注入的(后面会讲为什么这至关重要)。name="userForm":指定一个ActionForm(VO层对象),Struts会自动把请求参数(如?name=张三&id=100)填充进去,再传给Action。这是“参数绑定”的自动化。validate="true":表示执行Action前,先调用userForm.validate()方法校验参数(比如邮箱格式、密码长度)。校验失败直接跳inputforward,不进Action。
2.3 为什么Struts必须和Spring联手?单打独斗的致命短板
Struts自己有个硬伤:它管不了业务对象的生命周期。比如UserListAction里需要调用UserService,传统写法是:
public class UserListAction extends Action { public ActionForward execute(...) { UserService service = new UserServiceImpl(); // ❌ 硬编码!无法复用、无法测试、无法事务控制 List<User> users = service.findAll(); // ... } }问题来了:
- 每次请求都
new一个UserServiceImpl,内存爆炸; UserServiceImpl如果依赖UserDao,还得自己newDao,层层嵌套;- 最要命的是:
UserServiceImpl里的数据库操作,没法被Spring的事务管理器(@Transactional)包裹——转账操作一半失败,钱就丢了。
解决方案:让Spring接管所有Bean,包括Struts的Action。在applicationContext.xml里声明:
<!-- Spring配置:让Spring创建并管理Action --> <bean id="userListAction" class="com.simon.oa.user.controler.UserListAction" scope="prototype"> <!-- 注意:Action必须prototype,否则多线程共享状态! --> <property name="userService" ref="userService"/> <!-- 注入Service --> </bean>再在struts-config.xml里告诉Struts:“别自己new了,去Spring容器里拿!”:
<action path="/user/list" type="org.springframework.web.struts.DelegatingActionProxy" <!-- 关键! --> name="userForm"> <forward name="success" path="/user/list.jsp"/> </action>DelegatingActionProxy是个代理,它拦截请求,去Spring容器里找userListActionBean,再执行。从此,Action成了纯粹的“流程胶水”,所有业务逻辑、事务、日志、安全校验,全交给Spring管。这就是SSH组合的第一根钢钉:Struts负责“路怎么走”,Spring负责“谁来走、怎么走稳”。
3. 业务逻辑层:Spring 不是万能油,而是精密的“组件装配流水线”
如果说Struts是前台调度员,那Spring就是这家奶茶店的店长兼HR总监兼财务总监。它不管具体冲泡步骤(那是DAO的事),但确保:配方师(Service)有最新版秘方(配置),后厨(DAO)领料时走正规流程(事务),新员工(Bean)上岗前完成培训(初始化),所有动作都在监控下(AOP)。
3.1 IoC/DI:不是玄学,是“甩手掌柜式管理”
IoC(控制反转)听着高大上,说白了就一件事:把对象创建和依赖关系的控制权,从程序员手里,交给Spring容器这个“包工头”。
传统写法(程序员当包工头):
// 我要造一辆车 Engine engine = new Engine(); // 自己买发动机 Wheel wheel = new Wheel(); // 自己买轮胎 Car car = new Car(engine, wheel); // 自己组装Spring写法(包工头代劳):
<!-- applicationContext.xml --> <bean id="engine" class="com.simon.Engine"/> <bean id="wheel" class="com.simon.Wheel"/> <bean id="car" class="com.simon.Car"> <constructor-arg ref="engine"/> <!-- 包工头自动把engine塞给car --> <constructor-arg ref="wheel"/> <!-- 包工头自动把wheel塞给car --> </bean>程序员只需写:
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); Car car = (Car) ctx.getBean("car"); // 包工头已组装好,直接领走为什么这能救命?
- 解耦:
Car类里再也不用写new Engine(),它只关心“我需要一个Engine接口”,具体是V8Engine还是ElectricEngine,由配置决定。换动力系统?改XML就行,不碰Java代码。 - 可测试:单元测试
Car时,可以轻松注入MockEngine,不用真连发动机。 - 生命周期统一:
Engine如果是单例(scope="singleton"),全店共用一个;Car如果是原型(scope="prototype"),每次getBean()都给新车——Spring全管。
3.2 Service层:业务规则的“宪法”,不是代码堆砌场
Service层是三层架构的“大脑”,它必须干净、纯粹、无污染。看一个反面教材:
// ❌ 错误示范:Service里混入了表现层和持久层代码 public class UserServiceImpl implements UserService { public String login(String username, String password) { // 1. 直接操作HttpSession(表现层!) HttpSession session = request.getSession(); // 2. 手写JDBC(持久层!) Connection conn = DriverManager.getConnection(...); PreparedStatement ps = conn.prepareStatement("SELECT * FROM user WHERE ..."); // 3. 拼HTML(视图层!) return "<div class='success'>欢迎" + username + "</div>"; } }正确姿势(✅ 正确示范):
// ✅ Service只做三件事:校验规则、编排流程、调用DAO @Service // Spring注解,自动注册为Bean public class UserServiceImpl implements UserService { @Autowired // Spring自动注入DAO,非手动new private UserDao userDao; @Autowired // Spring自动注入密码加密工具 private PasswordEncoder passwordEncoder; @Transactional // Spring事务管理:整个方法要么全成功,要么全回滚 public User login(String username, String password) { // 1. 业务规则校验(空值、格式) if (StringUtils.isBlank(username)) { throw new BusinessException("用户名不能为空"); } // 2. 调用DAO获取用户(只返回POJO,不碰HTTP/DB细节) User user = userDao.findByUsername(username); if (user == null) { throw new BusinessException("用户不存在"); } // 3. 业务逻辑:密码比对(加密后的) if (!passwordEncoder.matches(password, user.getPasswordHash())) { throw new BusinessException("密码错误"); } // 4. 更新最后登录时间(仍是业务逻辑) user.setLastLoginTime(new Date()); userDao.update(user); // DAO负责具体SQL return user; // 返回纯数据对象,不带任何视图信息 } }关键点:
@Service:标记这是业务组件,Spring自动扫描注册。@Autowired:依赖由Spring注入,Service不关心DAO怎么实现(Hibernate/JDBC/Redis)。@Transactional:这是Spring的“后悔药”。如果userDao.update()之后抛异常,Spring自动回滚之前所有数据库操作,保证数据一致性。- 返回
User对象:这是领域模型(Domain Object),只含数据,不含<div>或HttpSession。
3.3 Spring如何“织入”横切关注点?AOP不是魔法,是“隐形监工”
日志、事务、权限检查、性能监控……这些需求贯穿所有Service方法,但写在每个方法里就是重复代码。AOP(面向切面编程)就是解决这个问题的“隐形监工”。
比如,想给所有UserService方法加日志:
// 定义一个“切面”(监工) @Aspect @Component public class LoggingAspect { // 定义“切点”:哪些方法需要被监控? @Pointcut("execution(* com.simon.oa.user.service..*.*(..))") public void userServiceMethods() {} // 定义“通知”:监控时做什么? @Before("userServiceMethods()") public void logBefore(JoinPoint joinPoint) { System.out.println("【日志】开始执行:" + joinPoint.getSignature().toShortString()); } @AfterReturning(pointcut = "userServiceMethods()", returning = "result") public void logAfter(JoinPoint joinPoint, Object result) { System.out.println("【日志】执行成功,返回:" + result); } }效果:UserService.login()、UserService.register()等所有方法,无需修改一行代码,自动拥有日志。Spring在运行时动态“织入”这些逻辑,就像给汽车出厂前自动贴上GPS和黑匣子。
注意:AOP代理默认是JDK动态代理(要求目标类实现接口),如果Service没接口,需开启CGLIB代理(
<aop:config proxy-target-class="true"/>)。这是初学者常踩的“代理失效”坑。
4. 数据访问层:Hibernate 不是SQL翻译器,而是“Java对象的数据库管家”
很多人以为Hibernate就是“把SQL写成HQL”,其实它真正的价值在于:让你彻底忘记“表”和“字段”,只和“对象”打交道。它不是ORM框架,它是“对象持久化管家”。
4.1 从数据库表到Java对象:映射不是翻译,是“建模”
假设有一张user表:
| id | username | created_time |
|---|
传统JDBC写法:
String sql = "SELECT id, username, email FROM user WHERE id = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setInt(1, 100); ResultSet rs = ps.executeQuery(); User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); // ... 手动赋值20行Hibernate做法:先定义一个User实体类(POJO),再写一个映射文件(User.hbm.xml):
<!-- User.hbm.xml --> <class name="com.simon.oa.user.model.User" table="user"> <id name="id" column="id"> <generator class="native"/> <!-- 主键自增策略 --> </id> <property name="username" column="username"/> <property name="email" column="email"/> <property name="createdTime" column="created_time" type="timestamp"/> </class>然后,DAO层代码极简:
// UserDaoImpl 继承 HibernateDaoSupport public class UserDaoImpl extends HibernateDaoSupport implements UserDao { public User findById(Long id) { return (User) getHibernateTemplate().get(User.class, id); // ✅ 传Class,不传SQL } public List<User> findAll() { return getHibernateTemplate().loadAll(User.class); // ✅ 查全部,不写SQL } public void save(User user) { getHibernateTemplate().save(user); // ✅ 保存对象,Hibernate自动生成INSERT } }关键洞察:User类不是“数据库快照”,而是业务领域的概念。createdTime是Date类型,不是String;id是Long,不是int。Hibernate在中间做了所有类型转换、SQL生成、连接管理。你改用MySQL或Oracle?只需改hibernate.dialect配置,Java代码零改动。
4.2 HQL:不是SQL的马甲,是“面向对象的查询语言”
HQL(Hibernate Query Language)查询的是对象和属性,不是表和字段:
// HQL:查询所有用户名包含"张"的User对象 String hql = "FROM User u WHERE u.username LIKE :name"; List<User> users = getHibernateTemplate().find(hql, "%张%"); // HQL:关联查询(User和Order是一对多) String hql2 = "SELECT u, o FROM User u JOIN u.orders o WHERE o.status = 'PAID'"; List<Object[]> results = getHibernateTemplate().find(hql2);对比SQL:
-- 等价SQL(你永远不用写) SELECT u.*, o.* FROM user u INNER JOIN order o ON u.id=o.user_id WHERE o.status='PAID';HQL优势:
- 数据库无关:
LIKE在MySQL/Oracle/SQL Server写法一致,Hibernate自动转译。 - 类型安全:
u.username是编译期检查的属性名,拼错IDE直接报红;SQL字符串里写错useranme,运行时才报错。 - 对象导航:
u.orders直接访问关联集合,不用手写JOIN。
4.3 Session:不是HttpSession,是“数据库会话的临时工作台”
Session是Hibernate最易混淆的概念。它不是javax.servlet.http.HttpSession(用户会话),而是Hibernate与数据库交互的一级缓存+工作单元。
- 一级缓存(Session级):同一Session内,多次
get(User.class, 1)只查一次数据库,后续直接从内存返回。这是性能优化,也是事务一致性的基础。 - 工作单元(Unit of Work):所有
save()、update()、delete()操作,都先记在Session的“待办清单”里,直到session.flush()或事务提交时,才批量生成SQL执行。这意味着:User user = session.get(User.class, 100); user.setEmail("new@xx.com"); // 修改内存对象 // 没有session.update()!Hibernate自动检测脏数据 transaction.commit(); // 此时才生成UPDATE语句
提示:
Session是线程不安全的,必须每个请求一个Session(通常由Spring的OpenSessionInViewFilter或@Transactional管理)。把它当全局变量用?并发下数据错乱是必然的。
5. 易混概念辨析:三层架构不是“三个文件夹”,SSH不是“三个Jar包”
初学者常把架构理解为物理目录或技术堆砌,结果写出“形似神散”的代码。以下5个误区,踩中一个,项目就埋下雷。
5.1 误区1:DAO层 = 写SQL的地方?
为什么混:看到UserDao就以为要手写SELECT * FROM user。
怎么区分:DAO层是数据访问契约,定义“我能做什么”(User findById(Long id)),不规定“怎么做”。具体实现可以是Hibernate(getHibernateTemplate().get())、MyBatis(selectUserById)、甚至Redis(redisTemplate.opsForValue().get())。只要接口不变,底层可随时切换。真正的SQL/HQL应藏在DAO实现类内部,Service层完全看不见。
5.2 误区2:Service层 = 把DAO方法串起来?
为什么混:以为Service就是userDao.findById()→orderDao.findByUserId()→productDao.findById()的流水线。
怎么区分:Service层必须封装业务规则和流程。例如“下单”:
- ✅ 正确:校验库存、扣减库存、生成订单、发送通知、更新用户积分——这是一个原子业务用例。
- ❌ 错误:只调用
inventoryDao.decrease()和orderDao.save(),把库存不足、通知失败等异常甩给上层处理。
Service是“业务专家”,不是“DAO调度员”。
5.3 误区3:VO/DTO/Entity傻傻分不清?
为什么混:都叫“对象”,都放get/set,干脆全用User类贯穿三层。
怎么区分(看用途):
| 类型 | 全称 | 所在层 | 特点 | 示例字段 |
|---|---|---|---|---|
| Entity | 实体类 | 数据访问层 | 严格对应数据库表,含@Id、@Column等JPA注解 | id,username,email,createdTime |
| DTO | Data Transfer Object | 服务间传输 | 轻量、无行为,用于跨层/跨系统传递数据,避免暴露Entity细节 | userId,userName,emailHash(脱敏) |
| VO | View Object | 表示层 | 专为前端展示定制,可含isCanEdit、formattedTime等计算属性 | userName,lastLoginStr,avatarUrl,statusBadge |
提示:绝对禁止在JSP里直接用
Entity对象!user.getPassword()可能被意外输出到HTML源码里。VO才是页面的“安全出口”。
5.4 误区4:SSH = Struts+Spring+Hibernate三个独立框架?
为什么混:分别下载三个Jar包,各自配各自的XML,结果Struts找不到Spring的Bean。
怎么区分:SSH是深度集成的有机体:
- Struts的
Action必须由Spring容器创建(DelegatingActionProxy); - Spring的
DataSource和SessionFactory必须注入到Hibernate; - Spring的事务管理器(
HibernateTransactionManager)必须包裹所有Service方法。
它们通过配置文件(applicationContext.xml、struts-config.xml、hibernate.cfg.xml)和约定(如HibernateDaoSupport)紧密咬合。少一个齿轮,整个系统卡死。
5.5 误区5:三层架构 = 必须用SSH?
为什么混:以为不用Struts就不叫MVC,不用Hibernate就不叫三层。
怎么区分:三层是思想,不是技术栈。
- 表示层可选:Spring MVC(更现代)、JSF、甚至纯REST API(Vue/React调用);
- 业务层可选:Spring Boot(自动配置)、Quarkus(云原生);
- 持久层可选:MyBatis(SQL可控)、JPA(标准规范)、JOOQ(类型安全SQL)。
SSH是特定时代的经典解法,但分层原则永恒:职责分离、松耦合、可替换。今天用Spring Boot + MyBatis,只要Controller→Service→Mapper分层清晰,就是合格的三层架构。
6. 什么时候用三层架构?什么时候该果断放弃?
三层架构不是银弹。我曾在一个只有3个接口的内部管理后台,强行套用SSH:搭环境花2天,写一个用户列表要配5个XML文件(struts-config.xml、applicationContext.xml、hibernate.cfg.xml、User.hbm.xml、UserDAO.hbm.xml),上线后运维抱怨“一个配置错,全站500”。后来砍掉Struts,用Spring MVC + JdbcTemplate,开发时间缩到半天,维护成本降为零。这件事让我明白:架构选择,本质是成本与收益的权衡。
6.1 适合三层架构的典型场景(必须用)
| 场景 | 为什么必须分层 | 架构带来的实际收益 |
|---|---|---|
| 企业级OA/ERP系统(如某通信公司问答系统) | 功能模块多(用户、权限、工单、报表)、团队超10人、需长期迭代 | - 后端改数据库字段,前端页面不受影响 - 前端换Vue,后端Service层零修改 - 新增“微信登录”模块,只在表示层加Controller,复用原有UserService |
| 高并发电商核心链路(如秒杀、下单) | 需精细化控制事务边界、缓存策略、降级开关 | - Service层用@Transactional(propagation=REQUIRES_NEW)隔离下单事务- DAO层可针对 InventoryDao单独加Redis缓存- 表示层 OrderController可配置熔断器,失败直接返回“稍后再试” |
| 多端统一后台(Web+App+小程序) | 同一套业务逻辑,需同时服务不同前端 | - Controller层提供REST API(@RestController)- Service层完全复用,不写任何 HttpServletRequest- VO层按端定制:App要 {code:0,data:{}},小程序要{errno:0,data:{}} |
6.2 不适合三层架构的场景(果断放弃)
| 场景 | 为什么不该用 | 更优方案 |
|---|---|---|
| 单页应用(SPA)的纯前端项目 | 没有服务端渲染,所有逻辑在浏览器 | 用Vue/React + Axios调用后端API,后端只需提供RESTful接口,无需Struts MVC |
| 脚本型工具(如日志分析脚本、数据库迁移工具) | 单次运行、无用户交互、无持久化需求 | 直接用Java Main方法 + JDBC,30行代码搞定,引入Spring是灾难 |
| 微服务中的边缘服务(如短信网关) | 功能单一(发短信)、性能敏感、需快速迭代 | Spring Boot + Netty(异步IO),省去Struts的HTTP解析开销,直接处理HTTP请求 |
6.3 一份真实的目录结构:不是教条,是血泪经验凝结的约定
某高校实验室为某通信公司问答系统制定的包结构,至今仍在多个项目中沿用。它不追求理论完美,只解决“新人接手不懵逼”:
com.simon.oa/ ├── controller/ # 表示层:Struts Action,命名规则 XxxAction │ └── user/ │ └── UserListAction.java ├── service/ # 业务逻辑层:接口+实现,命名规则 XxxService / XxxServiceImpl │ ├── IUserService.java │ └── impl/ │ └── UserServiceImpl.java ├── dao/ # 数据访问层:接口+实现,命名规则 XxxDao / XxxDaoImpl │ ├── IUserDao.java │ └── impl/ │ └── UserDaoImpl.java ├── model/ # Entity层:数据库实体,命名规则 Xxx(如User.java) │ ├── User.java │ └── User.hbm.xml # Hibernate映射文件 ├── vo/ # VO层:表示层专用对象,命名规则 XxxVO(如UserVO.java) │ └── UserVO.java └── dto/ # DTO层:跨服务传输对象,命名规则 XxxDTO(如UserDTO.java) └── UserDTO.java关键约定(来自踩坑总结):
controller包名用controller而非action:强调职责(控制流程),而非技术(Struts Action);service和dao必须接口+实现分离:IUserService定义契约,UserServiceImpl可被Mock测试;model只放Entity,绝不放业务逻辑方法(如user.calculateScore())——那是Service的事;vo和dto物理分离:VO给JSP用,DTO给远程调用用,避免“一个对象到处传”导致的安全泄露。
从那以后我每次启动新项目,第一件事就是按这个结构建包,然后强制走一遍:
- 写一个
UserEntity和User.hbm.xml; - 写
IUserDao接口和UserDaoImpl实现; - 写
IUserService和UserServiceImpl,注入IUserDao; - 写
UserListAction,注入IUserService; - 配
struts-config.xml和applicationContext.xml,确保Action能拿到Service。
只要这五步通了,后面加功能就是复制粘贴。希望帮到你。
本文还有配套的精品资源,点击获取