简介:Struts2是用于构建企业级Java Web应用的主流框架,遵循MVC设计模式,具有模块化、可扩展性强等特点。本文通过经典的“HelloWorld”案例,系统讲解Struts2的核心组件与工作流程,包括Action类的定义、struts.xml配置、拦截器机制、结果类型映射以及视图渲染。读者将通过完整的代码示例和运行步骤,掌握Struts2的基本开发模式,为深入学习Web层框架打下坚实基础。
Struts2框架核心概念与MVC架构解析
在现代Java Web开发中,我们常常会遇到这样的问题:为什么一个简单的表单提交,背后却要写一堆看似重复的代码? 🤔 为什么页面跳转总是那么“脆弱”,稍微改个名字就404了?其实啊,这些问题的背后,正是因为我们缺少一套 清晰、规范、可维护 的开发模式。而Struts2,就是为了解决这类痛点而诞生的经典MVC框架。
它不像Spring那样包罗万象,也不像Servlet那样原始裸露,而是介于两者之间——既提供了足够的抽象来提升效率,又保留了对底层流程的控制力。可以说,Struts2是早期Java EE时代Web开发的一把“瑞士军刀”。🛠️ 尽管如今它的风头已被Spring Boot盖过,但在许多遗留系统和教学场景中,依然是不可绕开的重要一环。
那今天咱们就不走寻常路,不照本宣科地念定义,而是从一个最朴素的问题出发:
当用户点击“登录”按钮时,Struts2到底干了啥?
带着这个问题,我们一起深入Struts2的世界,看看它是如何通过MVC分层、拦截器机制和OGNL表达式,把一次HTTP请求变成一次优雅的应用逻辑执行的。
Struts2框架概述:不只是过滤器那么简单
提到Struts2,很多人第一反应是 struts.xml 或者Action类,但真正掌控全局的,其实是那个默默无闻的前端控制器—— StrutsPrepareAndExecuteFilter 。这个名字有点长,但它做的事情非常关键: 拦截所有进入应用的HTTP请求,并启动整个MVC流程 。
你可能会问:“这不就是一个Filter吗?Servlet里也能写啊。”
没错,但它不是普通的Filter,它是整个Struts2宇宙的“大爆炸起点”。💥
这个Filter会在容器启动时加载 struts-default.xml (内置配置),然后读取你的 struts.xml ,构建出一张完整的“路由地图”。每当有请求进来,比如 /user/login.action ,它就会根据这张地图找到对应的Action类,创建实例,调用方法,最后决定跳转到哪个页面。
整个过程就像一位经验丰富的导游,全程带你游览后台世界的每一个角落,而且还不让你迷路。
举个例子:
<action name="login" class="com.example.LoginAction">
<result name="success">/dashboard.jsp</result>
<result name="input">/login.jsp</result>
</action>
当你访问 /login.action 时,Struts2会自动实例化 LoginAction ,调用其 execute() 方法。如果返回 "success" ,就跳转到仪表盘;如果是 "input" ,说明校验失败,回到登录页。
这一切都不需要你在代码里手动写 request.getRequestDispatcher().forward() ,完全由框架帮你调度完成。👏
更妙的是,这套机制基于 插件化设计 ,你可以自定义拦截器、结果类型、类型转换器……几乎每个环节都可以扩展。这就让Struts2既有“开箱即用”的便利性,又有“深度定制”的灵活性。
MVC在Struts2中的实现:谁才是真正的主角?
说到MVC,大家都知道是Model-View-Controller三剑客。但在Struts2中,这三个角色的分工有点“反直觉”。
Model:不只是数据载体,更是业务中枢
传统理解中,Model就是POJO(Plain Old Java Object),比如User、Order这种实体类。但在Struts2里, Action类本身承担了Model的角色 !
也就是说,你的 LoginAction 不仅接收参数、处理逻辑,还保存状态信息。它既是“控制器”,又是“模型”。
public class LoginAction extends ActionSupport {
private String username;
private String password;
public String execute() {
// 业务逻辑在这里
if (userService.authenticate(username, password)) {
return SUCCESS;
} else {
addActionError("账号或密码错误");
return ERROR;
}
}
// getter/setter...
}
看到没?这个类既有属性(username/password),又有行为(execute),完全是典型的面向对象设计。而且这些属性会被自动填充,无需手动 request.getParameter() 。
这就是Struts2的聪明之处:把Action当作一个“活”的模型对象,而不是冷冰冰的数据传输容器。
View:JSP + 标签库 = 动态视图引擎
视图层通常是JSP页面,配合Struts2提供的标签库使用。这些标签不仅仅是HTML生成工具,更是与值栈(ValueStack)交互的桥梁。
比如这一行:
<s:property value="username"/>
看起来只是输出用户名,实际上它是在OGNL上下文中查找 username 属性。而这个属性可能来自Action,也可能来自ValueStack中的其他对象。
更重要的是,Struts2标签自带主题支持,默认使用 xhtml 主题,能自动生成带label、error提示的完整表单结构,省去了大量手写HTML的麻烦。
当然啦,如果你追求极致自由,也可以切到 simple 主题,去掉所有额外包装,自己用CSS重新设计样式。🎨
Controller:隐形的指挥官
控制器在Struts2中并不是某个具体的类,而是由 框架内核 + 拦截器链 共同构成的一个“分布式控制器”。
它没有显式的 if-else 路由判断,而是通过 struts.xml 中的配置来声明式地定义流程。比如:
<interceptor-ref name="validation"/>
<interceptor-ref name="workflow"/>
这两个拦截器组合起来,就能实现“先校验再执行”的控制流。如果没有错误,继续往下走;如果有错,直接跳转回input页面,连 execute() 都不调用。
这种AOP式的控制方式,比传统的if-else干净太多了。✅
核心组件协同机制:一场精密的交响乐演奏
想象一下,一次HTTP请求进入Struts2应用,就像一位观众走进音乐厅。接下来会发生什么?
- 入场检票 :
StrutsPrepareAndExecuteFilter拦截请求,确认是否属于本应用管辖范围; - 引导入座 :根据URL匹配namespace和action name,定位到目标Action;
- 准备乐器 :拦截器链依次执行,比如参数绑定、文件上传、权限检查;
- 正式演奏 :调用Action的
execute()方法,执行业务逻辑; - 谢幕退场 :根据返回结果,选择对应视图进行渲染,最终将HTML送回浏览器。
整个过程行云流水,各个组件各司其职,彼此解耦却又紧密协作。而这其中最关键的“指挥棒”,就是 值栈(ValueStack)和OGNL表达式语言 。
我们可以画个简图来直观感受一下这个流程:
graph TD
A[HTTP Request] --> B{StrutsPrepareAndExecuteFilter}
B --> C[创建ValueStack]
C --> D[执行拦截器链]
D --> E[params拦截器注入参数]
E --> F[validation拦截器校验数据]
F --> G{是否有错误?}
G -->|是| H[返回input视图]
G -->|否| I[执行Action.execute()]
I --> J[返回result字符串]
J --> K[查找Result映射]
K --> L[渲染JSP视图]
L --> M[Response输出]
是不是感觉特别清晰?每一站都有明确的任务,每一步都可追踪、可调试。
尤其是那个 值栈(ValueStack) ,简直是Struts2的灵魂所在。它就像一个临时仓库,把Action、Model、Session等各种数据统统放进去,供OGNL随时取用。
而OGNL呢?它就像是一个超级查询语言,不仅能访问属性、调用方法,还能做集合投影、条件判断,甚至调用静态方法!🤯
有了这对黄金搭档,JSP页面再也不用写Java脚本了,真正做到“展示逻辑与业务逻辑分离”。
等等,说到OGNL和Action,这部分内容太重要了,值得单独拎出来细讲。咱们不妨换个角度,从开发者日常编码的实际体验出发,深入聊聊 Action类的设计哲学与OGNL的魔法世界 。
毕竟,这才是我们每天打交道最多的地方,也是最容易写出“屎山代码”的重灾区。😅
Action类的设计与OGNL表达式应用
有没有觉得有时候写Action就像在填表格?字段一个接一个,getter/setter复制粘贴,validate方法写得头晕眼花……到最后自己都忘了哪个字段对应哪个校验规则。🙈
别急,这说明你还没真正掌握Struts2的设计精髓。让我们从最基础的问题开始:
为什么我们要继承ActionSupport?不能直接new一个Action出来吗?
好问题!这背后其实藏着一条重要的软件工程原则: 组合优于继承?还是继承优于组合?
2.1 ActionSupport基类的作用与继承机制
2.1.1 Action接口与ActionSupport的区别
首先,我们得明白一件事:Struts2的Action本质上是一个 约定接口 ,而不是强制契约。
原始的 Action 接口长得极简:
public interface Action {
String SUCCESS = "success";
String NONE = "none";
String ERROR = "error";
String INPUT = "input";
String LOGIN = "login";
String execute() throws Exception;
}
就这么点东西。你要实现它,就得自己搞定所有事情——错误收集、国际化消息、日志记录、输入校验……全靠手工打造。
但现实项目哪有这么理想?每个Action都要重复写 List<String> errors = new ArrayList<>() ?累不死你也烦死你。
于是, ActionSupport 出现了。它不是一个接口,而是一个 提供了默认实现的抽象基类 。
public class ActionSupport implements Action, Validateable, ValidationAware, TextProvider, LocaleProvider {
protected List<String> actionErrors;
protected List<String> fieldErrors;
protected Map<String, Object> actionMessages;
public void validate() { }
public String execute() throws Exception {
return SUCCESS;
}
public boolean hasErrors() { ... }
public void addActionError(String error) { ... }
}
看到没?它一口气实现了五个关键接口:
- Validateable :支持校验
- ValidationAware :能感知错误状态
- TextProvider :提供文本资源访问能力
- LocaleProvider :获取当前语言环境
- 还有最基本的 Action
这意味着只要你继承了 ActionSupport ,立马就拥有了:
- 错误信息添加 ( addActionError )
- 字段级校验 ( addFieldError )
- 国际化文本获取 ( getText )
- 空实现的 validate() 方法
简直是“开箱即用”的典范!📦
| 特性 | Action接口 | ActionSupport |
|---|---|---|
| 实现方式 | 必须手动实现全部逻辑 | 提供默认实现 |
| 错误处理 | 需自行维护错误列表 | 内置 addActionError() 等方法 |
| 校验支持 | 无内置validate() | 提供空实现的 validate() 方法 |
| 国际化支持 | 不支持 | 实现 TextProvider 接口 |
| 返回值常量 | 定义基本常量 | 继承并扩展使用 |
所以说, ActionSupport 不是为了“强制你继承”,而是为了 减少样板代码,提升开发效率 。这才是它的真正价值所在。
不过话说回来,有人可能会担心:“这不是违反了‘组合优于继承’的原则吗?”
嗯,问得好。但请注意, ActionSupport 并没有强迫子类必须覆盖构造函数或初始化流程,而是依赖Struts2容器通过反射和依赖注入来装配上下文。因此,它更像是一个 功能聚合体 ,而非严格的继承关系。💡
2.1.2 validate()与execute()方法的执行流程
接下来,我们来揭开一个经常被误解的谜团:
validate() 和 execute() 到底谁先执行?它们是怎么联动的?
很多人以为这是Action自己的行为,其实不然。这一切都是由 拦截器链 控制的!
具体来说,是两个拦截器联手完成的:
- validation 拦截器:负责调用 validate() 方法
- workflow 拦截器:负责决定是否跳过 execute()
下面是它们协作的真实流程:
graph TD
A[请求进入] --> B{Action是否实现Validateable?}
B -->|是| C[调用validate()]
B -->|否| D[跳过校验]
C --> E{是否有错误?}
E -->|有| F[设置result=input]
E -->|无| G[执行execute()]
F --> H[跳转至input视图]
G --> I[返回结果]
I --> J[查找对应result视图]
注意几个关键点:
1. 只有实现了 Validateable 接口的类才会触发 validate() ,而 ActionSupport 正好实现了它;
2. validation 拦截器运行在 params 之后,确保参数已经注入完毕再进行校验;
3. 如果发现错误(比如调用了 addFieldError() ), workflow 拦截器会中断流程,直接返回 input 结果,根本不会进入 execute() 。
这就避免了“明明表单都没填对,还跑去数据库查一遍”的尴尬局面。👏
来看个真实例子:
public class LoginAction extends ActionSupport {
private String username;
private String password;
public void validate() {
if (username == null || username.trim().isEmpty()) {
addFieldError("username", "请输入用户名");
}
if (password == null || password.length() < 6) {
addFieldError("password", "密码至少6位");
}
}
public String execute() {
if ("admin".equals(username) && "123456".equals(password)) {
return SUCCESS;
} else {
addActionError("登录失败,请检查账号密码");
return ERROR;
}
}
}
这段代码的执行路径可能是:
- 用户提交空表单 → validate() 添加错误 → 跳转回登录页显示红字提示;
- 用户密码太短 → 同样走 input 路径;
- 用户名密码正确 → validate() 无错 → 执行 execute() → 成功跳转;
- 用户名密码错误 → execute() 返回 ERROR → 显示全局错误。
完美闭环!🎯
而且,Struts2还支持 方法级校验 。比如你有个Action有多个方法:
public void validateSave() { ... }
public String save() { return SUCCESS; }
public void validateDelete() { ... }
public String delete() { return SUCCESS; }
只有当调用 save.action 时,才会触发 validateSave() ,避免不必要的校验开销。
2.1.3 数据校验与错误信息封装实践
说到校验,光会用 addFieldError 还不够,你还得知道怎么组织错误信息才够专业。
编程式校验 vs 声明式校验
Struts2提供两种校验方式:
- 编程式校验 :在 validate() 方法里写Java代码,适合复杂业务规则;
- 声明式校验 :用XML或注解定义规则,适合通用约束(如非空、长度、邮箱格式)。
对于新手,建议先掌握编程式校验,因为它更直观,也更容易调试。
比如跨字段校验:
@Override
public void validate() {
if (username != null && username.equals(password)) {
addFieldError("password", "密码不能与用户名相同");
}
if (!StringUtils.contains(email, "@")) {
addFieldError("email", "邮箱格式不正确");
}
}
这些错误信息最终都会被放入ValueStack,供JSP页面使用。
国际化支持:让错误提示说“人话”
硬编码错误消息是非常危险的行为。想想看,万一哪天产品经理说:“我们要做国际化版本!” 你就得满代码找字符串替换。
所以正确的做法是: 把错误消息外置到资源文件中 。
创建 ApplicationResources.properties :
username.required=用户名不能为空
password.tooShort=密码长度不能少于6位
中文版 ApplicationResources_zh_CN.properties :
username.required=请输入用户名
password.tooShort=密码至少6位
然后在Action中引用:
addFieldError("username", getText("username.required"));
getText() 会根据当前用户的Locale自动选择合适的语言版本,轻松实现多语言切换。
错误信息展示方式对比
| 错误类型 | 添加方法 | JSP标签 | 用途说明 |
|---|---|---|---|
| 字段错误 | addFieldError() | <s:fielderror> | 显示具体字段的校验错误 |
| 动作错误 | addActionError() | <s:actionerror> | 显示整体操作失败原因 |
| 消息提示 | addActionMessage() | <s:actionmessage> | 显示成功或其他提示信息 |
合理搭配使用,可以做出用户体验极佳的反馈界面。👍
2.2 OGNL表达式在Action数据传递中的角色
如果说Action是演员,那OGNL就是舞台上的聚光灯——它决定了观众(JSP页面)能看到什么、怎么看到。
2.2.1 OGNL上下文结构与值栈(ValueStack)原理
OGNL全称是Object-Graph Navigation Language,听着很高大上,其实就是一种 对象图导航语言 。它可以让你用一行表达式访问深层嵌套的对象属性,甚至调用方法、操作集合。
而在Struts2中,OGNL的上下文就是 ValueStack 。
每个请求都会有一个独立的ValueStack实例,它包含两部分:
- Root栈 :存放Action、Model等对象,OGNL默认从此处查找;
- Context映射 :包含request、session、parameters等作用域变量。
graph TB
subgraph ValueStack
root[Root Stack]
context[Context Map]
root --> action1[HelloAction]
root --> model[UserModel]
context --> session[Session Scope]
context --> request[Request Scope]
context --> parameters[Parameters]
context --> attr[Page/Application Attributes]
end
当你在JSP中写:
<s:property value="name"/>
Struts2会去ValueStack的Root栈顶找 name 属性。如果找不到,再去Context里找,比如 #session.user.name 。
这种设计的好处是: 你不需要显式地把数据塞进request域 ,只要放在Action里,就能被自动访问。
2.2.2 在JSP中通过OGNL访问Action属性
OGNL的强大之处在于它的表达式语法。下面是一些常用技巧:
| 表达式 | 说明 |
|---|---|
name | 访问Action的 getName() 方法 |
user.address.city | 多层嵌套属性访问 |
users[0].name | 访问List集合第一个元素的name属性 |
scores['math'] | 访问Map类型的scores中键为”math”的值 |
friends.{name} | 投影操作:提取所有friend对象的name组成新列表 |
items.{? #this.price > 100} | 选择操作:筛选价格大于100的项 |
举个实际例子:
public class ProductAction extends ActionSupport {
private List<Product> products;
public String execute() {
products = Arrays.asList(
new Product("iPhone", 999),
new Product("iPad", 799),
new Product("iMac", 1999)
);
return SUCCESS;
}
public List<Product> getProducts() { return products; }
}
在JSP中可以这样渲染:
<!-- 显示所有产品名称 -->
<s:iterator value="products.{name}" var="n">
<p><s:property/></p>
</s:iterator>
<!-- 显示高价产品 -->
<s:iterator value="products.{? #this.price > 800}">
<p><s:property value="name"/>: ¥<s:property value="price"/></p>
</s:iterator>
看到了吗?不用写Java代码,仅靠OGNL就能完成集合的筛选和投影。这在JSP中简直是降维打击!😎
2.2.3 类型转换与集合操作的OGNL实现
Struts2内置了强大的类型转换机制。比如前端传过来的字符串 "true" ,能自动转成boolean; "2024-01-01" 能转成Date。
甚至连复选框都能自动组装成List:
<input type="checkbox" name="hobbies" value="reading"/>
<input type="checkbox" name="hobbies" value="music"/>
只要Action中有:
private List<String> hobbies;
public void setHobbies(List<String> hobbies) { this.hobbies = hobbies; }
Struts2就会把多个同名参数合并为一个List,自动赋值。✨
此外,OGNL还支持动态构造对象:
<s:set var="now" value="@java.util.Calendar@getInstance().time"/>
当前时间:<s:date name="#now" format="yyyy-MM-dd HH:mm:ss"/>
这里调用了静态方法获取当前时间,并格式化输出。
常见OGNL操作对照表
| 操作类型 | 示例表达式 | 说明 |
|---|---|---|
| 静态方法调用 | @Class@method() | 如 @Math@random() |
| 静态字段访问 | @Class@FIELD | 如 @java.lang.Boolean@TRUE |
| 构造新对象 | new java.util.ArrayList() | 可在表达式内创建实例 |
| 条件运算 | score > 80 ? '优秀' : '一般' | 支持三元操作符 |
| 方法调用 | userName.toUpperCase() | 调用对象方法 |
这些特性极大增强了视图层的表现力,减少了Java代码的侵入性。
2.3 实践:构建HelloWorld对应的Action类
理论说了这么多,不如动手写个HelloWorld来得实在。🚀
2.3.1 编写继承ActionSupport的HelloAction
package com.example.action;
import com.opensymphony.xwork2.ActionSupport;
public class HelloAction extends ActionSupport {
private String message;
private String name;
@Override
public String execute() throws Exception {
if (name == null || name.trim().isEmpty()) {
name = "World";
}
message = "Hello, " + name + "!";
return SUCCESS;
}
// Getter and Setter
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
重点解释:
- 第7–8行:定义属性,用于接收参数和存储结果;
- 第11–17行: execute() 中判断 name 是否为空,设置默认值并构造问候语;
- 第19–30行:标准JavaBean的getter/setter方法,供OGNL访问。
注意:Struts2通过反射调用setter方法注入参数,所以必须保证setXxx方法存在。
2.3.2 定义业务逻辑与返回结果常量
虽然可以直接写 return "success"; ,但最好统一使用常量:
public class Constants {
public static final String SUCCESS = "success";
public static final String INPUT = "input";
public static final String ERROR = "error";
}
不过更常见的做法是直接继承 ActionSupport 里的常量:
return INPUT; // 通常用于返回表单页
也可以自定义结果类型:
<result name="welcome">/WEB-INF/pages/welcome.jsp</result>
并在Action中返回:
return "welcome";
2.3.3 调试Action执行过程的日志输出
为了观察执行流程,可以在 struts.xml 中启用开发模式:
<constant name="struts.devMode" value="true"/>
同时添加日志输出:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class HelloAction extends ActionSupport {
private static final Log LOG = LogFactory.getLog(HelloAction.class);
@Override
public String execute() {
LOG.info("开始执行HelloAction.execute()");
LOG.debug("接收到name参数:" + name);
// 业务逻辑...
LOG.info("生成消息:" + message);
return SUCCESS;
}
}
确保日志配置正确后,访问 /hello.action?name=Tom ,你会在控制台看到:
INFO HelloAction - 开始执行HelloAction.execute()
DEBUG HelloAction - 接收到name参数:Tom
INFO HelloAction - 生成消息:Hello, Tom!
这对排查参数未注入、方法未执行等问题非常有帮助。🔍
综上所述,Action类设计与OGNL表达式的深度融合构成了Struts2的核心竞争力。掌握这两者的工作机制,不仅能提升开发效率,更能深入理解框架背后的运行逻辑,为后续拦截器、标签库等高级特性的学习打下坚实基础。💪
struts.xml配置与请求映射机制
如果说Action是演员,OGNL是灯光,那 struts.xml 就是整场演出的 剧本和导演手册 。🎭
它决定了谁在什么时候出场,说什么台词,最后走向哪个结局。没有它,整个MVC流程就乱套了。
可问题是,这份“剧本”该怎么写?怎么才能让它既清晰又灵活?别急,咱们一步步来拆解。
3.1 struts.xml文件的基本结构与DTD约束
struts.xml 是Struts2的主配置文件,通常放在 src/main/resources 目录下。它是整个应用的“中枢神经”,负责定义:
- 请求路径映射
- Action类绑定
- 结果跳转逻辑
- 拦截器栈引用
- 命名空间管理
一个典型的配置长这样:
<!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
"http://struts.apache.org/dtds/struts-2.5.dtd">
<struts>
<package name="default" extends="struts-default" namespace="/">
<action name="hello" class="com.example.HelloAction">
<result name="success">/WEB-INF/jsp/welcome.jsp</result>
</action>
</package>
</struts>
我们逐行来看:
| 元素 | 参数 | 说明 |
|---|---|---|
<!DOCTYPE ... > | - | 声明 DTD 约束版本,确保 XML 结构符合 Struts2 规范 |
<struts> | - | 根元素,所有配置都必须嵌套在其内部 |
<package> | name | 包名,唯一标识一组 Action |
extends | 继承另一个包(通常是 struts-default )以复用拦截器栈等配置 | |
namespace | 定义该包下所有 Action 的 URL 前缀路径 | |
<action> | name | 请求 URL 中的 action 名称(如 /hello.action ) |
class | 对应的 Action 类全限定名 | |
method | 可选,指定调用的具体方法,默认为 execute() | |
<result> | name | 返回结果逻辑名,需与 Action 中返回值匹配 |
| 内容体 | 实际跳转路径,支持 JSP、FreeMarker、Velocity 等 |
⚠️ 注意:若未显式声明
class,则默认使用ActionSupport;若未指定method,则调用execute()方法。
这个配置的意思是:当访问 /hello.action 时,实例化 HelloAction 并执行 execute() ,如果返回 "success" ,就跳转到 /WEB-INF/jsp/welcome.jsp 。
整个流程如下图所示:
graph TD
A[HTTP Request: /hello.action] --> B{StrutsPrepareAndExecuteFilter}
B --> C[查找匹配的 Package]
C --> D[根据 namespace 和 action name 定位 Action]
D --> E[实例化 HelloAction]
E --> F[执行 execute() 方法]
F --> G{返回结果字符串}
G -- "success" --> H[查找对应 <result>]
H --> I[转发至 welcome.jsp]
G -- "input" --> J[跳转输入页或报错]
是不是特别清晰?每一步都有迹可循。
3.1.2 namespace命名空间的作用与配置策略
随着项目变大,你会发现一个问题:不同模块都想用 list.action 、 edit.action 这种通用名字,怎么办?难道要起名叫 userList.action 、 productList.action ?太丑了!
这时候, namespace 就派上用场了。它就像Java的包机制,给Action划分“行政区”。
例如:
<package name="admin" namespace="/admin" extends="struts-default">
<action name="list" class="com.example.AdminAction">
<result>/WEB-INF/jsp/admin/list.jsp</result>
</action>
</package>
<package name="user" namespace="/user" extends="struts-default">
<action name="list" class="com.example.UserAction">
<result>/WEB-INF/jsp/user/list.jsp</result>
</action>
</package>
现在:
- /admin/list.action → Admin模块
- /user/list.action → User模块
完美解决命名冲突!
而且查找还有优先级规则:
1. 精确匹配优先
2. 逐级回溯(如 /admin/sub 找不到会尝试 /admin )
3. 最后 fallback 到根命名空间
✅ 最佳实践建议:
- 使用 /module 形式(如 /product , /order )提高语义清晰度;
- 避免嵌套过深(不超过两级);
- 将公共工具类 Action 放入根命名空间。
3.1.3 默认action与默认result处理机制
为了让系统更健壮,Struts2提供了两种“兜底”机制:
(1)默认 Action(default-action-ref)
当某个命名空间下没有匹配的 Action 时,可以指定一个默认处理程序:
<package name="public" namespace="/site" extends="struts-default">
<default-action-ref name="notFound"/>
<action name="home"><result>/home.jsp</result></action>
<action name="about"><result>/about.jsp</result></action>
<action name="notFound"><result>/error/404.jsp</result></action>
</package>
访问 /site/nonexistent.action 就会自动跳转到 404 页面。
(2)通配符 Action(Wildcard Mapping)
想不想批量映射一堆Action?比如 /api/user/save 、 /api/order/create ?
可以用通配符:
<package name="api" namespace="/api" extends="struts-default">
<action name="*/*" class="com.example.{1}Action" method="{2}">
<result>/WEB-INF/json.jsp</result>
</action>
</package>
访问 /api/user/save 就会调用 UserAction.save() 方法,简直不要太爽!😄
3.2 Action请求映射与路径解析规则
3.2.1 URL到Action方法的匹配逻辑
当用户发起请求如 http://localhost:8080/app/hello.action 时,Struts2会:
1. 提取 contextPath、namespace 和 actionName
2. 遍历所有 package 查找匹配项
3. 实例化 Action 并执行目标方法
.action 是默认扩展名,可通过 struts.action.extension 修改,甚至设为空实现伪静态。
3.2.2 method属性指定自定义方法调用
除了 execute() ,还可以直接指定其他方法:
<action name="saveUser" class="com.example.UserAction" method="save"/>
<action name="deleteUser" class="com.example.UserAction" method="delete"/>
这样就能避免在一个Action里塞太多方法,保持职责单一。
3.2.3 动态方法调用(DMI)的安全性考量
Struts2曾支持通过URL直接调用方法:
/hello!sayHi.action
但这带来了严重的安全风险(S2-057漏洞),因此强烈建议关闭DMI:
<constant name="struts.enable.DynamicMethodInvocation" value="false"/>
改用通配符或 method= 属性替代。
3.3 实践:配置HelloWorld应用的请求路由
3.3.1 创建package并声明action映射
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
"http://struts.apache.org/dtds/struts-2.5.dtd">
<struts>
<constant name="struts.devMode" value="true"/>
<package name="helloworld" namespace="/hw" extends="struts-default">
<action name="hello" class="com.demo.HelloAction">
<result name="success">/WEB-INF/pages/welcome.jsp</result>
</action>
</package>
</struts>
3.3.2 设置success结果跳转至JSP页面
创建 welcome.jsp :
<%@ taglib prefix="s" uri="/struts-tags" %>
<h2><s:property value="message"/></h2>
3.3.3 验证struts.xml语法正确性的调试技巧
- 启用
devMode查看日志 - 使用
struts2-config-browser-plugin可视化查看配置 - 检查
web.xml是否注册了StrutsPrepareAndExecuteFilter
搞定!🎉
拦截器机制与结果类型控制
终于到了Struts2最精彩的部分—— 拦截器(Interceptor) 。
它就像电影里的“幕后英雄”,默默完成日志记录、权限校验、事务管理等工作,却不抢主角(Action)的风头。
4.1 拦截器工作原理剖析
4.1.1 拦截器栈与执行生命周期钩子
拦截器采用“环绕通知”模式,在Action执行前后插入逻辑:
public String intercept(ActionInvocation invocation) throws Exception {
System.out.println("Before Action");
String result = invocation.invoke();
System.out.println("After Action");
return result;
}
多个拦截器组成“拦截器栈”,按顺序执行,后置处理逆序执行(LIFO)。
内置的 defaultStack 包含参数绑定、校验、国际化等功能,开箱即用。
4.1.2 常用拦截器详解
| 拦截器 | 功能 |
|---|---|
params | 自动注入请求参数 |
modelDriven | 将Model置于ValueStack顶端 |
validation | 执行字段校验 |
顺序很重要! modelDriven 必须在 params 之前,否则无法正确绑定。
4.1.3 自定义拦截器开发
比如实现权限检查:
public class RoleCheckInterceptor extends AbstractInterceptor {
private String requiredRole;
@Override
public String intercept(ActionInvocation invocation) throws Exception {
String userRole = (String) ActionContext.getContext().getSession().get("role");
if (!requiredRole.equals(userRole)) {
return "forbidden";
}
return invocation.invoke();
}
public void setRequiredRole(String requiredRole) {
this.requiredRole = requiredRole;
}
}
注册后即可在Action中使用:
<interceptor-ref name="roleCheck">
<param name="requiredRole">ADMIN</param>
</interceptor-ref>
参数通过setter自动注入,超方便!🔧
4.2 结果类型多样化配置
4.2.1 dispatcher、redirect、chain对比
| 类型 | 行为 | 地址栏变化 | 共享Request |
|---|---|---|---|
| dispatcher | 服务器转发 |
简介:Struts2是用于构建企业级Java Web应用的主流框架,遵循MVC设计模式,具有模块化、可扩展性强等特点。本文通过经典的“HelloWorld”案例,系统讲解Struts2的核心组件与工作流程,包括Action类的定义、struts.xml配置、拦截器机制、结果类型映射以及视图渲染。读者将通过完整的代码示例和运行步骤,掌握Struts2的基本开发模式,为深入学习Web层框架打下坚实基础。
613

被折叠的 条评论
为什么被折叠?



