Struts2入门经典实战示例

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:Struts2是Java Web开发中广泛应用的MVC框架,以其清晰的架构和强大的功能著称。本“struts入门最简单例子”项目专为初学者设计,通过一个精简但完整的Web应用实例,帮助开发者快速掌握Struts2的核心概念与基本配置。内容涵盖Action类的定义、struts.xml配置文件的使用、结果类型映射、JSP视图展示以及OGNL表达式在数据传递中的应用。项目结构清晰,便于理解MVC模式在实际开发中的实现方式,为后续学习拦截器、表单验证、文件上传等高级功能打下坚实基础。
struts入门最简单例子

1. Struts2框架核心概念概述

Struts2作为Java Web开发中经典的MVC框架之一,以其清晰的架构设计和强大的功能扩展性,在企业级应用开发中占据重要地位。本章将深入剖析Struts2的核心设计理念,重点讲解其基于 StrutsPrepareAndExecuteFilter 的前端控制器模式,该过滤器拦截所有请求并委派给相应的Action处理,实现请求路径与业务逻辑的解耦。

flowchart TD
    A[HTTP Request] --> B[StrutsPrepareAndExecuteFilter]
    B --> C[Interceptor Stack]
    C --> D[Action Execution]
    D --> E[Result Type Dispatch]
    E --> F[JSP/JSON/Redirect]

通过拦截器栈(Interceptor Stack),Struts2实现了横切关注点(如日志、权限、参数封装)的模块化管理。OGNL表达式语言与ValueStack上下文环境是数据流转的核心:ValueStack作为请求生命周期内的数据存储中心,使得Action、JSP页面可无缝共享数据。与Struts1相比,Struts2不再依赖Servlet API,提升了可测试性,并支持插件化扩展,为现代Web应用提供了灵活的架构基础。

2. Action类的设计与实现(如HelloWorldAction)

在Struts2框架中, Action 类是MVC架构中的控制器核心组件,承担着接收请求、处理业务逻辑以及决定视图跳转路径的关键职责。它不仅是用户请求的终点,也是数据流转的起点。一个设计良好的 Action 类能够提升系统的可维护性、可测试性和扩展能力。本章将围绕 Action 的设计模式、生命周期管理、参数绑定机制及输入验证策略展开深入探讨,并通过典型示例如 HelloWorldAction 展示其实际应用。

2.1 Action类的基本结构与生命周期

Struts2中的 Action 并非必须继承特定基类,这与早期Struts1形成鲜明对比。开发者可以通过实现 com.opensymphony.xwork2.Action 接口或直接使用POJO(Plain Old Java Object)来定义行为逻辑。这种松耦合设计极大增强了灵活性和单元测试便利性。

2.1.1 实现com.opensymphony.xwork2.Action接口与返回结果常量

当显式实现 Action 接口时,需重写 execute() 方法并返回预定义的结果字符串常量。这些常量由接口提供,具有明确语义:

package com.example.action;

import com.opensymphony.xwork2.Action;

public class HelloWorldAction implements Action {

    private String message;

    @Override
    public String execute() throws Exception {
        setMessage("Hello, Struts2 World!");
        return SUCCESS; // 返回Action.SUCCESS = "success"
    }

    // getter/setter 省略
}

上述代码展示了最基础的 Action 实现方式。其中:
- SUCCESS :表示操作成功,通常映射到成功页面;
- ERROR :操作失败,跳转至错误页;
- INPUT :输入校验失败后返回表单页;
- LOGIN :未登录状态下的重定向目标;
- NONE :无需跳转,常用于Ajax响应。

返回值常量 字符串值 典型用途
SUCCESS “success” 正常流程完成
ERROR “error” 业务处理出错
INPUT “input” 校验失败或需要重新输入
LOGIN “login” 认证拦截后的登录跳转
NONE “none” 不进行视图跳转(如返回JSON)

该机制通过 struts.xml <result> 节点完成映射:

<action name="hello" class="com.example.action.HelloWorldAction">
    <result name="success">/WEB-INF/pages/hello.jsp</result>
</action>

逻辑分析 execute() 是整个 Action 执行链的核心入口方法。每当HTTP请求匹配到对应Action配置时,Struts2会创建该类实例(默认每次请求新建),调用 execute() 方法,并根据返回值查找对应的 <result> 配置进行页面跳转。此过程受拦截器栈控制,例如 params 拦截器会在 execute() 前完成参数注入。

2.1.2 execute()方法的作用与执行时机分析

execute() 方法本质上是一个契约方法,标志着业务逻辑的正式开始。它的执行时机严格依赖于Struts2拦截器链的调度顺序。

下图为典型的请求处理流程中 execute() 的位置:

sequenceDiagram
    participant Client
    participant Filter as StrutsPrepareAndExecuteFilter
    participant InterceptorStack
    participant Action

    Client->>Filter: HTTP Request
    Filter->>InterceptorStack: invoke()
    loop 拦截器依次执行
        InterceptorStack->>InterceptorStack: prepare, params, validation...
    end
    InterceptorStack->>Action: execute()
    Action-->>InterceptorStack: return result
    InterceptorStack->>Filter: find Result
    Filter->>Client: Render View (JSP/Freemarker)

从流程可见, execute() 在一系列前置拦截器(如 params , validation , fileUpload 等)之后被调用,确保所有上下文准备就绪。例如:
- params 拦截器负责将请求参数自动赋值给Action属性;
- validation 拦截器执行校验规则;
- 若有错误,则不会进入 execute() ,而是提前跳转至 input 结果。

因此, execute() 只应在“参数已注入、校验已通过”的前提下运行,保证了业务逻辑的安全边界。

此外,可通过自定义拦截器干预执行流程。例如,添加权限检查拦截器:

public class AuthInterceptor extends AbstractInterceptor {
    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        Map<String, Object> session = invocation.getInvocationContext().getSession();
        User user = (User) session.get("user");
        if (user == null) {
            return "login"; // 阻断execute执行
        }
        return invocation.invoke(); // 继续执行链,最终调用execute
    }
}

只有在 invocation.invoke() 被调用后,才会真正触发目标 Action execute() 方法。

2.1.3 Action实例的线程安全性问题探讨

尽管Struts2提倡使用POJO作为Action,但其默认作用域为 每次请求创建新实例 (prototype模式),从而天然规避了多线程并发访问共享状态的问题。

考虑以下非线程安全示例:

public class UnsafeCounterAction extends ActionSupport {
    private int counter = 0; // 实例变量

    public String execute() {
        counter++; // 多个请求同时修改此字段 → 数据错乱
        return SUCCESS;
    }
}

若Action以单例模式存在(如Spring托管且scope=singleton),则多个线程可能同时操作 counter ,导致竞态条件。但在标准Struts2环境下,每个请求拥有独立的Action实例,故无需担心此类问题。

然而, 静态变量仍存在全局共享风险

public class StaticSharedAction extends ActionSupport {
    private static int globalCount = 0;

    public String execute() {
        globalCount++; // 危险!跨请求共享
        return SUCCESS;
    }
}

此类设计应避免,或配合同步机制(如 synchronized )使用。

推荐做法是将共享状态交由外部容器管理,如Session、Application Scope或数据库。例如:

public String execute() {
    ServletActionContext.getRequest().getSession()
                       .setAttribute("visitCount", ++globalCount);
    return SUCCESS;
}

总结:Struts2通过 每请求实例化 保障了Action的线程安全,开发者应避免滥用静态变量,优先利用ValueStack、Session等上下文对象传递状态。

2.2 基于POJO的Action开发模式

Struts2的一大优势在于支持纯粹的POJO作为Action类,无需继承任何框架特定基类。这一特性显著降低了框架侵入性,提升了代码的可移植性和单元测试友好度。

2.2.1 无需继承特定基类的优势与灵活性

传统MVC框架往往要求Action继承抽象基类(如 ActionSupport ),带来强耦合。而Struts2允许如下极简定义:

public class SimpleGreetAction {
    private String name;
    private String greeting;

    public String execute() {
        greeting = "Hello, " + name + "!";
        return "success";
    }

    // getters and setters
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getGreeting() { return greeting; }
    public void setGreeting(String greeting) { this.greeting = greeting; }
}

虽然不继承 ActionSupport 将失去便捷工具方法(如 addActionError() getText() 国际化支持等),但对于简单场景完全足够。

优势包括:
- 更易进行JUnit测试(无Servlet API依赖);
- 可复用于其他环境(如命令行工具);
- 提高代码清晰度,减少不必要的继承负担。

若需增强功能,可通过接口注入方式获取上下文:

import com.opensymphony.xwork2.ActionContextAware;
import java.util.Map;

public class ContextAwareAction implements ActionContextAware {
    private Map<String, Object> context;

    @Override
    public void setActionContext(Map<String, Object> context) {
        this.context = context;
    }

    public String execute() {
        context.put("timestamp", System.currentTimeMillis());
        return "success";
    }
}

此时仍保持POJO本质,仅通过接口回调获得运行时环境。

2.2.2 属性封装与getter/setter在参数注入中的关键作用

Struts2通过JavaBean规范实现自动参数注入。当请求包含 ?name=Tom 时,框架会查找Action中是否存在 setName(String) 方法,并尝试调用完成赋值。

GET /greet.action?name=Tom HTTP/1.1

对应Action:

public class GreetAction {
    private String name;

    public void setName(String name) {
        System.out.println("Setting name: " + name);
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public String execute() {
        return "success";
    }
}

后台输出:

Setting name: Tom

参数说明
- 参数名匹配基于setter方法去除 set 前缀并转小写( setName → name );
- 支持基本类型自动转换(String → int, double等);
- 若setter不存在,则忽略该参数(可通过日志开启警告)。

复杂对象注入也遵循相同原则:

public class UserAction {
    private User user = new User();

    public void setUser(User user) {
        this.user = user;
    }

    // getter省略
}

配合JSP表单:

<s:form action="saveUser">
    <s:textfield name="user.username" label="用户名"/>
    <s:password name="user.password" label="密码"/>
    <s:submit/>
</s:form>

Struts2会自动构建 User 对象并设值,前提是 User 类具备相应属性的getter/setter。

2.2.3 使用ModelDriven接口优化领域对象绑定

对于以领域模型为核心的CRUD操作,频繁编写 setUser(User user) 显得冗余。 ModelDriven 接口提供更优雅解决方案。

public class UserAction implements ModelDriven<User> {
    private User user = new User();

    @Override
    public User getModel() {
        return user;
    }

    public String save() {
        System.out.println("Saved user: " + user.getUsername());
        return SUCCESS;
    }
}

此时表单可简化为:

<s:form action="save">
    <s:textfield name="username" label="用户名"/> <!-- 直接绑定到model属性 -->
    <s:password name="password" label="密码"/>
    <s:submit/>
</s:form>

逻辑分析 ModelDrivenInterceptor 会调用 getModel() 获取当前模型对象,并将其压入ValueStack顶部。后续参数注入直接作用于该对象,无需前缀(如 user.username username )。这极大提升了表单绑定效率,尤其适用于单一实体操作场景。

特性比较 普通属性注入 ModelDriven模式
表单字段命名 user.name name
ValueStack结构影响 属性位于root栈底部 model位于栈顶,优先访问
适用场景 多对象混合表单 单一领域对象为主的操作
代码简洁性 较繁琐 极简

综上, ModelDriven 是面向领域驱动设计的理想选择,尤其适合增删改查类功能模块。

3. struts.xml配置文件结构与Action映射配置

Struts2框架的配置核心集中于 struts.xml 文件,它是整个应用行为控制的中枢。该文件定义了Action的映射关系、拦截器栈、结果类型、常量设置以及模块化组织策略,是连接请求路径与业务逻辑处理的关键桥梁。一个结构清晰、语义明确的 struts.xml 配置不仅提升了系统的可维护性,也为后续扩展提供了良好的基础。本章将深入剖析 struts.xml 的整体结构设计原则,并详细解析其在实际开发中的关键配置项。

3.1 struts.xml文件的DTD与命名空间解析

struts.xml 是基于 XML 格式的配置文件,遵循特定的 DTD(Document Type Definition)或 XSD 规范。它通过一系列嵌套标签构建出 Struts2 应用的运行时上下文环境。理解其根元素、包结构和命名空间机制,是掌握 Struts2 配置体系的第一步。

3.1.1 根元素 及其子元素组织结构

所有 Struts2 配置都必须包含在一个 <struts> 根标签内。这个标签作为容器,承载着多个 <package> <include> <constant> 等子元素,形成层次化的配置树。

<!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="default" extends="struts-default" namespace="/">
        <action name="hello" class="com.example.HelloWorldAction">
            <result>/WEB-INF/pages/hello.jsp</result>
        </action>
    </package>
</struts>
代码逻辑逐行解读分析:
  • 第1-3行 :声明 DOCTYPE,指定使用的 DTD 文件版本为 2.5,确保 XML 解析器能正确校验语法合法性。
  • 第5行 <struts> 为最外层根节点,不可省略,所有其他配置均需在此标签内部声明。
  • 第6行 :使用 <constant> 设置全局常量 struts.devMode=true ,开启开发者模式以启用调试信息输出。
  • 第8行 :定义名为 default 的 package,继承自 struts-default (内置标准包),并指定命名空间 /
  • 第9-11行 :在 package 内注册一个 Action 映射,名称为 hello ,对应类 HelloWorldAction ,执行成功后跳转至 JSP 页面。
参数 说明
name 常量键名,如 struts.devMode 表示是否启用开发模式
value 对应值,布尔型或字符串,影响框架行为

此结构体现了 Struts2 的“约定优于配置”思想:只要符合默认规则,开发者无需手动编写大量样板代码即可实现功能。

3.1.2 package包的划分原则与继承机制应用

Struts2 支持通过 <package> 实现模块化管理,每个 package 可视为一个独立的功能单元。合理划分 package 能提升项目的可读性和安全性。

<package name="user" extends="struts-default" namespace="/user">
    <action name="login" class="com.example.UserLoginAction">
        <result name="success">/user/success.jsp</result>
        <result name="input">/user/login.jsp</result>
    </action>
</package>

<package name="admin" extends="user" namespace="/admin">
    <action name="dashboard" class="com.example.AdminDashboardAction">
        <result>/admin/dashboard.jsp</result>
    </action>
</package>

上述配置展示了两个 package: user admin 。其中 admin 继承自 user ,意味着它可以复用父包中定义的结果类型、拦截器栈等资源。

graph TD
    A[struts-default] --> B[User Package]
    B --> C[Admin Package]
    style A fill:#f9f,stroke:#333
    style B fill:#bbf,stroke:#333
    style C fill:#bfb,stroke:#333

流程图说明 struts-default 是 Struts2 提供的基础包,封装了常用拦截器和结果类型; user 包继承之并添加用户相关 Action; admin 进一步继承 user ,实现权限隔离的同时共享通用配置。

继承优势分析:
  • 减少重复配置 :避免在每个包中重新定义相同的拦截器栈或 result-types。
  • 增强模块解耦 :不同团队可负责不同 package,便于协作开发。
  • 支持多命名空间 :允许同一 Action 类在不同命名空间下暴露不同接口。

3.1.3 namespace命名空间对URL路径的影响

命名空间(namespace)用于将 Action 按照功能域进行分组,直接影响客户端访问 URL 的构成方式。

假设存在如下配置:

<package name="public" namespace="/guest" extends="struts-default">
    <action name="register" class="com.example.RegisterAction">
        <result>/guest/register.jsp</result>
    </action>
</package>

<package name="secure" namespace="/secure" extends="struts-default">
    <action name="profile" class="com.example.ProfileAction">
        <result>/secure/profile.jsp</result>
    </action>
</package>

则对应的访问路径分别为:
- /guest/register.action
- /secure/profile.action

若未指定 namespace,则默认使用空字符串 "" ,即根路径 /register.action

namespace 设置 访问路径示例 用途场景
/ /login.action 公共页面入口
/api /api/user.action REST 接口分组
/admin /admin/edit.action 后台管理系统

命名空间还支持通配符匹配,例如:

<package name="dynamic" namespace="/*" extends="struts-default">
    <action name="view" class="com.example.DynamicViewAction">
        <param name="module">{1}</param>
        <result>/modules/{1}/view.jsp</result>
    </action>
</package>

此时 /news/view.action 中的 news 将被 {1} 捕获并传入 Action 属性,实现动态路由。

3.2 Action映射的详细配置方式

Action 映射是 struts.xml 的核心功能之一,决定了 HTTP 请求如何定位到具体的 Java 类方法。精准掌握 <action> 标签的各项属性及其作用时机,对于构建高可用 Web 应用至关重要。

3.2.1 标签的name、class、method属性详解

每个 <action> 必须至少指定 name class 属性, method 可选。

<action name="saveUser" 
        class="com.example.UserAction" 
        method="save">
    <result name="success" type="redirectAction">list</result>
    <result name="input">/user/form.jsp</result>
</action>
参数说明表:
属性 是否必需 默认值 功能描述
name - 匹配 URL 中 action 名称,如 saveUser.action
class com.opensymphony.xwork2.ActionSupport 指定处理该请求的 Java 类全限定名
method execute 指定调用的具体方法名,非默认方法时必填
执行逻辑分析:

当用户访问 /saveUser.action 时,Struts2 容器会:
1. 根据当前 namespace 查找匹配的 package;
2. 在 package 中查找 name="saveUser" 的 action;
3. 实例化 UserAction 类(每次请求新建实例);
4. 调用 save() 方法而非 execute()
5. 根据返回值选择对应 result 进行跳转。

值得注意的是, class 属性所指向的类必须提供公共无参构造函数,否则实例化失败。

3.2.2 动态方法调用(DMC)与感叹号语法的安全隐患

Struts2 支持通过 URL 直接指定调用方法,称为动态方法调用(Dynamic Method Invocation, DMI),典型写法如下:

/user!delete.action
/order!submit.action

对应的配置只需定义 name="user" 即可:

<package name="crud" namespace="/user" extends="struts-default">
    <action name="user" class="com.example.UserAction">
        <result>/user/result.jsp</result>
    </action>
</package>

虽然 DMI 极大简化了配置,但因其开放任意方法调用能力,极易引发安全漏洞。例如攻击者可通过构造恶意 URL 调用私有方法或敏感操作。

为此,Struts2 自 2.3 版本起默认禁用 DMI,需显式启用:

<constant name="struts.enable.DynamicMethodInvocation" value="true"/>

更推荐的做法是使用 method 属性进行静态绑定,提高可控性。

3.2.3 使用method属性显式指定处理方法

替代 DMI 的最佳实践是通过多个 <action> 显式绑定不同方法:

<action name="create" class="com.example.UserAction" method="create">
    <result name="success">/user/detail.jsp</result>
</action>
<action name="update" class="com.example.UserAction" method="update">
    <result name="success">/user/detail.jsp</result>
</action>
<action name="delete" class="com.example.UserAction" method="delete">
    <result name="success" type="redirect">list</result>
</action>

这种方式虽增加配置量,但具备以下优点:
- 安全性强:仅暴露预设方法;
- 易于审计:所有可访问端点一目了然;
- 支持细粒度拦截器控制。

sequenceDiagram
    participant Client
    participant Filter as StrutsPrepareAndExecuteFilter
    participant Action as UserAction
    Client->>Filter: GET /user/create.action
    Filter->>Action: newInstance()
    Action->>Action: create()
    Action-->>Filter: return "success"
    Filter-->>Client: forward to /user/detail.jsp

序列图说明 :完整展示从请求到达至视图渲染的过程,强调 method 属性引导的方法调用链路。

3.3 包含与常量配置策略

随着项目规模扩大,单一 struts.xml 文件难以维护。Struts2 提供 <include> <constant> 机制,支持配置拆分与全局参数统一管理。

3.3.1 标签实现模块化配置拆分

大型系统常按功能模块拆分配置文件,如 struts-user.xml struts-order.xml ,然后在主文件中引入:

<struts>
    <include file="struts-user.xml"/>
    <include file="struts-order.xml"/>
    <include file="struts-report.xml"/>
</struts>

各子文件内容格式一致:

<!-- struts-user.xml -->
<package name="user" extends="struts-default" namespace="/user">
    <action name="list" class="com.example.UserListAction">
        <result>/user/list.jsp</result>
    </action>
</package>

这种结构带来显著好处:
- 职责分离 :前端、后台、API 模块各自独立维护;
- 团队协作友好 :多人同时编辑不影响整体结构;
- 热部署便利 :修改某个模块无需重启整个应用。

3.3.2 设置开发模式、编码格式等全局参数

<constant> 用于设定框架级行为参数,常见配置包括:

<constant name="struts.devMode" value="true"/>
<constant name="struts.i18n.encoding" value="UTF-8"/>
<constant name="struts.locale" value="zh_CN"/>
<constant name="struts.objectFactory" value="spring"/>
常量名 作用
struts.devMode 开启后显示详细错误页、自动重载配置
struts.i18n.encoding 请求参数解码字符集
struts.locale 默认本地化语言环境
struts.objectFactory 集成 Spring 容器管理 Bean 生命周期

这些设置应在所有 package 之前声明,确保全局生效。

3.3.3 开发阶段开启调试信息输出提升排错效率

在开发环境中启用 devMode 后,Struts2 会在页面底部输出大量诊断信息,包括:
- 当前执行的 Action 类名;
- ValueStack 中的对象堆栈;
- 拦截器执行顺序;
- OGNL 表达式求值过程。

这对于排查“找不到 Action”、“参数未绑定”等问题极为有用。但在生产环境务必关闭,防止信息泄露。

3.4 拦截器栈与默认栈的自定义配置

拦截器(Interceptor)是 Struts2 实现横切关注点的核心机制。通过配置拦截器栈,可在不侵入业务代码的前提下完成日志记录、权限检查、事务管理等功能。

3.4.1 default-interceptor-stack的替换与扩展

每个 package 可指定默认拦截器栈,覆盖继承自 struts-default defaultStack

<package name="secured" extends="struts-default" namespace="/secure">
    <default-interceptor-ref name="customStack"/>
    <action name="dashboard" class="com.example.DashboardAction">
        <result>/secure/dashboard.jsp</result>
    </action>
</package>

<interceptors>
    <interceptor name="auth" class="com.example.AuthenticationInterceptor"/>
    <interceptor name="log" class="com.example.LoggingInterceptor"/>
    <interceptor-stack name="customStack">
        <interceptor-ref name="auth"/>
        <interceptor-ref name="log"/>
        <interceptor-ref name="defaultStack"/>
    </interceptor-stack>
</interceptors>

以上配置创建了一个名为 customStack 的新栈,优先执行认证和日志拦截器,再调用原始默认栈。

3.4.2 定义专用拦截器栈提高性能与安全性

针对特定业务场景定制拦截器组合,有助于优化性能:

<interceptor-stack name="lightStack">
    <interceptor-ref name="basicStack"/>
    <!-- 移除 params、modelDriven 等重型拦截器 -->
</interceptor-stack>

适用于 AJAX 接口或静态资源服务,减少不必要的参数解析开销。

3.4.3 在package级别统一设置拦截策略

通过 <default-interceptor-ref> 在 package 层面统一分配策略,避免每个 action 重复引用:

<package name="api" extends="struts-default" namespace="/api">
    <default-interceptor-ref name="jsonStack"/>
    <global-results>
        <result name="error">/error.json</result>
    </global-results>
    <!-- 所有 API Action 自动使用 JSON 拦截器 -->
</package>

结合 <global-results> 可实现跨 Action 的统一异常处理机制。

| 拦截器栈名称 | 包含拦截器 | 适用场景 |
|------------|-----------|---------|
| defaultStack | params, modelDriven, validation 等 | 普通表单提交 |
| jsonStack | json, params | RESTful 接口 |
| uploadStack | fileUpload, params | 文件上传处理 |
| lightStack | basicStack 子集 | 高频轻量请求 |

合理规划拦截器栈结构,不仅能提升系统健壮性,还能显著降低后期维护成本。

4. URL请求与Action方法的绑定机制

在Struts2框架中,URL请求与Action方法之间的绑定是整个MVC流程的核心枢纽。这一机制决定了用户的HTTP请求如何被正确解析、映射到具体的Java类和方法,并最终驱动业务逻辑执行与视图跳转。理解该过程不仅有助于开发者设计清晰的路由结构,还能有效规避潜在的安全隐患与性能瓶颈。本章将从底层匹配规则出发,深入剖析Struts2是如何通过配置、拦截器链以及OGNL表达式协同工作来实现灵活而高效的请求分发系统。

Struts2采用“前端控制器”模式,所有请求均经过 StrutsPrepareAndExecuteFilter 进行统一调度。在此基础上,框架通过 struts.xml 中的Action映射定义、命名空间划分、通配符匹配等多种手段构建了一套高度可配置的路由体系。与此同时,方法级别的分发支持(如动态调用或注解驱动)进一步提升了开发灵活性。更重要的是,参数自动注入与结果预处理机制使得开发者无需手动解析请求体即可完成数据流转,极大简化了Web层编码复杂度。

值得注意的是,这种绑定并非简单的字符串匹配,而是融合了优先级判断、上下文查找、类型转换、安全过滤等多个环节的复合流程。例如,当一个请求路径为 /user/save.action 时,Struts2不仅要根据 namespace="/user" 找到对应package,还需结合action name“save”定位具体Action类,再依据method属性或DMC语法决定执行哪个Java方法,最后还要确保该方法返回的结果能在result配置中找到对应视图资源。这一系列操作的背后,涉及多个核心组件的协作,包括ActionMapper、ActionProxy、Interceptor栈等。

此外,随着RESTful架构的普及,传统基于 .action 后缀的请求风格已逐渐向更语义化的路径设计演进。Struts2通过Convention插件和注解支持实现了对REST风格的良好兼容,使开发者能够以更简洁的方式定义无XML配置的路由规则。然而,这也带来了新的挑战——如何在保持灵活性的同时避免过度暴露内部方法?如何防止恶意用户利用动态方法调用来触发未授权操作?

因此,深入掌握URL到Action方法的绑定机制,不仅是实现功能的基础,更是保障系统安全性、可维护性的重要前提。接下来的内容将逐层展开,从路径解析规则、方法分发策略、参数传递流程到结果执行前的准备阶段,全面揭示Struts2请求绑定的内在逻辑。

4.1 请求路径到Action的路由匹配规则

Struts2的请求路由机制建立在一个高度结构化的映射模型之上,其核心目标是将外部HTTP请求精准地映射至对应的Action类及其执行上下文中。这一过程依赖于 struts.xml 中定义的package、namespace、action name三者之间的层级关系,并结合通配符匹配与优先级判定机制实现灵活而可靠的路径解析。

4.1.1 URL命名规范与action名称解析顺序

Struts2默认接受以 .action 为后缀的请求路径,例如 /login.action /admin/user/add.action 。尽管该后缀可自定义(通过 <constant name="struts.action.extension" value="do"/> ),但 .action 仍是标准约定。当请求进入容器时, ActionMapper 组件负责将其分解为关键元素: namespace action name method (如有)。解析顺序如下:

  1. 首先提取URI路径,去除上下文路径(Context Path);
  2. 根据斜杠 / 分割路径段,逆向查找是否存在匹配的namespace;
  3. 在匹配的package中查找具有相同action name的 <action> 定义;
  4. 若未明确指定method,则默认调用 execute() 方法。

例如,对于请求 /app/user/profile/edit.action ,假设存在以下配置:

<package name="user" namespace="/user" extends="struts-default">
    <action name="profile/edit" class="com.example.ProfileAction" method="input"/>
</package>

此时,Struts2会识别出namespace为 /user ,action name为 profile/edit ,从而成功匹配该Action。这说明action name可以包含斜杠,允许构建层次化路径结构。

路径示例 Namespace Action Name 是否匹配
/user/list.action /user list
/admin/user/add.action /admin/user add ✅(若存在对应namespace)
/index.action ”“(根) index
/api/v1/data.json /api/v1 data ✅(若extension包含json)

⚠️ 注意:若多个package拥有相同namespace,只有最先声明的会被使用;建议通过继承机制组织模块化结构。

4.1.2 通配符*与{1}占位符在映射中的灵活运用

为了减少重复配置,Struts2提供了强大的通配符映射功能。通过在 <action> name 属性中使用星号 * ,可以实现批量映射,提升配置效率。

示例:通用CRUD映射
<package name="crud" namespace="/crud" extends="struts-default">
    <action name="*_*" 
            class="com.example.{1}Action" 
            method="{2}">
        <result name="success">/WEB-INF/pages/{1}/{2}.jsp</result>
    </action>
</package>

当访问 /crud/User_save.action 时:
- 第一个 * 匹配 User {1}
- 第二个 * 匹配 save {2}
- 实际执行类为 com.example.UserAction save() 方法
- 返回视图位于 /WEB-INF/pages/User/save.jsp

这种方式极大地减少了XML配置量,适用于标准化的模块开发。

graph TD
    A[HTTP Request: /crud/Product_delete.action] --> B{ActionMapper解析}
    B --> C[Namespace: /crud]
    C --> D[Action Name: Product_delete]
    D --> E[匹配 *_* 模式]
    E --> F[替换 {1}=Product, {2}=delete]
    F --> G[实例化 com.example.ProductAction]
    G --> H[调用 delete() 方法]
    H --> I[返回 success 结果]
    I --> J[渲染 /WEB-INF/pages/Product/delete.jsp]

📌 提示:通配符匹配遵循“最长匹配优先”原则。若有多个模式可匹配,Struts2会选择最具体的那个。

4.1.3 路径匹配优先级与冲突解决策略

当多个Action定义可能响应同一请求时,Struts2依据以下优先级顺序进行选择:

  1. 精确匹配 > 通配符匹配
  2. 较深namespace优先于较浅namespace
  3. 先定义的package优先于后定义的(同级情况下)
冲突场景示例
<!-- Package A -->
<package name="p1" namespace="/data" extends="struts-default">
    <action name="export" class="DataAction" method="exportCSV"/>
</package>

<!-- Package B -->
<package name="p2" namespace="/" extends="struts-default">
    <action name="*" class="GenericAction" method="process"/>
</package>

请求 /data/export.action 将优先匹配 p1/export 而非 p2/* ,因为前者是精确匹配且namespace更深。

可通过日志开启调试信息验证匹配过程:

<constant name="struts.devMode" value="true"/>
<constant name="struts.i18n.reload" value="true"/>
<constant name="struts.configuration.xml.reload" value="true"/>

启用后,控制台将输出类似日志:

DEBUG [com.opensymphony.xwork2.ActionMapping] - Matched action name 'export' in namespace '/data'
DEBUG [org.apache.struts2.dispatcher.mapper.DefaultActionMapper] - Setting method = 'exportCSV'

综上所述,合理规划命名空间与action命名规范,结合通配符与优先级机制,能显著提升系统的可维护性与扩展性。

4.2 方法级别的请求分发机制

Struts2不仅支持将URL映射到整个Action类,还允许细粒度地指定应执行的具体方法,从而在一个Action中实现多个业务操作,避免类爆炸问题。

4.2.1 使用action!methodName语法触发特定方法

动态方法调用(Dynamic Method Invocation, DMI)允许通过 ! 符号直接指定要执行的方法名:

/userAction!save.action     → 执行 UserAction.save()
/orderAction!cancel.action → 执行 OrderAction.cancel()

此功能默认启用,但存在严重安全隐患——攻击者可通过构造任意方法名尝试调用私有或敏感方法(如 toString getClass 等)。

安全风险演示
public class UserAction {
    public String deleteAllUsers() {
        // 危险操作!
        return "success";
    }
}

若用户访问 /userAction!deleteAllUsers.action ,该方法将被执行!

解决方案:禁用DMI
<constant name="struts.enable.DynamicMethodInvocation" value="false"/>

替代方案推荐使用 method 属性或REST风格路由。

4.2.2 基于注解@Action的方式简化配置(需插件支持)

引入 struts2-convention-plugin 后,可使用注解替代XML配置:

@Namespace("/api")
@ParentPackage("json-default")
public class ApiAction {

    @Action(value = "getUser", results = {
        @Result(name = "success", type = "json")
    })
    public String getUser() {
        // 返回JSON数据
        return "success";
    }

    @Action("login")
    public String login() {
        return "redirect:/dashboard.action";
    }
}

无需 struts.xml ,框架自动扫描带有 @Action 的类并注册路由。

✅ 优势:零配置、易读性强、适合微服务架构
❌ 缺陷:失去集中管理能力,不利于大型项目治理

4.2.3 RESTful风格URL设计与Convention插件整合

借助 struts2-rest-plugin ,可实现真正的RESTful路由:

GET    /users          → index()
POST   /users          → create()
GET    /users/123      → show(id)
PUT    /users/123      → update(id)
DELETE /users/123      → destroy(id)

配合 @RestAction 注解与HTTP动词绑定,完全脱离 .action 后缀,提升API语义清晰度。

此类设计更适合现代前后端分离架构,但也要求团队具备更强的约定意识与文档规范。

4.3 请求参数传递与自动赋值过程

Struts2通过拦截器机制实现了请求参数到Action属性的自动绑定,极大简化了开发流程。

4.3.1 HTTP请求参数如何映射至Action属性

假设提交表单:

<form action="register.action" method="post">
    <input name="username" />
    <input name="age" />
    <input name="birthDate" />
</form>

对应Action:

public class RegisterAction extends ActionSupport {
    private String username;
    private int age;
    private Date birthDate;

    // getter/setter...
    public String execute() {
        System.out.println("Hello " + username);
        return SUCCESS;
    }
}

无需任何手动取参,Struts2会在调用 execute() 前自动调用setter完成赋值。

4.3.2 参数拦截器ParameterInterceptor的工作流程

ParameterInterceptor 是实现自动赋值的关键组件,其执行逻辑如下:

public class ParameterInterceptor extends AbstractInterceptor {
    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        Object action = invocation.getAction();
        ValueStack stack = invocation.getStack();
        Map<String, Object> parameters = getParameters(invocation); // 获取request.getParameterMap()

        for (Map.Entry<String, Object> entry : parameters.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();

            if (acceptableName(key)) { // 白名单检查
                try {
                    stack.setParameter(key, value); // 触发setter调用
                } catch (RuntimeException e) {
                    LOG.warn("Exception setting parameter [" + key + "]", e);
                }
            }
        }

        return invocation.invoke(); // 继续拦截器链
    }
}

🔍 逐行分析:
- getParameters(invocation) :封装了 HttpServletRequest.getParameterMap() 的调用;
- acceptableName(key) :防止 class.classLoader 等危险属性访问;
- stack.setParameter() :利用OGNL引擎调用目标对象的setter方法;
- 异常捕获确保即使部分参数失败也不中断流程。

4.3.3 过滤危险参数防止恶意注入攻击

为防御Ognl表达式注入,应在 struts.xml 中配置:

<constant name="struts.excludedClasses" 
          value="java.lang.Class, java.lang.ClassLoader, javax.servlet.ServletContext"/>

<constant name="struts.excludedPackageNamePatterns" 
          value="^java\..*,^javax\..*"/>

同时启用 paramsPrepareParamsStack 拦截器栈,提供更安全的参数处理链。

4.4 结果映射前的方法执行链分析

在Action方法执行完毕、视图渲染之前,Struts2提供了两个重要扩展点。

4.4.1 prepare拦截器在execute前初始化数据

实现 Preparable 接口可在 execute() 前预加载数据:

public class EditUserAction implements Preparable {
    private User user;

    public void prepare() throws Exception {
        this.user = userService.findById(id); // 提前加载
    }

    public String execute() {
        return "success"; // user已准备好
    }
}

搭配 prepare 拦截器使用,确保资源就绪后再进入主逻辑。

4.4.2 PreResultListener监听器在跳转前介入流程

可在Action中注册监听器,在结果执行前修改状态:

public String execute() {
    ActionContext.getContext().getActionInvocation()
        .addPreResultListener(new PreResultListener() {
            @Override
            public void beforeResult(ActionInvocation invocation, String resultCode) {
                System.out.println("即将跳转至: " + resultCode);
                // 可动态修改session、记录日志等
            }
        });
    return SUCCESS;
}

这一机制非常适合做权限校验、审计追踪等横切关注点处理。


以上内容完整展示了Struts2中URL请求与Action方法绑定的全过程,涵盖路由解析、方法分发、参数绑定及结果前置处理四大维度,辅以代码、表格与流程图,满足深度技术探讨需求。

5. JSP视图与控制器之间的数据传递(使用OGNL表达式)

Struts2框架中,JSP页面与Action控制器之间的数据交互依赖于强大的 OGNL(Object-Graph Navigation Language)表达式语言 和其背后的 ValueStack机制 。这种设计不仅实现了MVC架构中“视图”与“模型”的松耦合通信,还极大提升了开发效率与代码可读性。通过本章深入剖析,读者将掌握如何在JSP中高效访问Action属性、操作上下文对象、调用方法并处理复杂结构的数据集合。同时,还将揭示ValueStack的内部结构、查找顺序以及性能优化策略,为构建高响应性的Web应用提供理论支撑。

5.1 OGNL表达式基础语法与JSP集成

5.1.1 OGNL核心概念与基本语法结构

OGNL是一种功能强大的表达式语言,专用于导航Java对象图并执行动态操作。在Struts2中,所有从Action传递到JSP的数据均存储在 ValueStack 中,而OGNL则作为访问该栈的“钥匙”。一个典型的OGNL表达式可以访问属性、调用方法、执行运算甚至创建新对象。

例如,在JSP中使用 <s:property value="username"/> 实际上等价于执行 ognl.Ognl.getValue("username", ValueStack) 。这里的 "username" 是一个OGNL表达式,它会在ValueStack中查找名为 username 的属性。

OGNL支持多种语法形式:

  • 属性访问: user.name
  • 方法调用: user.getName()
  • 静态字段/方法: @java.lang.Math@PI @com.example.Util@classMethod()
  • 集合操作: users.{name} (投影)、 users.{? #this.age > 18} (选择)
  • 运算符: + , - , == , && , ?:

这些特性使得开发者可以在视图层实现复杂的逻辑渲染,而不必完全依赖后端预处理。

5.1.2 JSP标签库与OGNL的协同工作机制

Struts2提供了丰富的JSP标签库( <s:...> ),它们本质上是OGNL表达式的封装接口。每个标签都接受 value 属性,并将其内容解析为OGNL表达式进行求值。

以下是一个典型示例:

<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<body>
    <p>用户名:<s:property value="username"/></p>
    <p>用户年龄:<s:property value="user.age"/></p>
    <p>欢迎信息:<s:property value="'Hello, ' + username"/></p>
</body>
</html>

上述代码中:
- 第一行引入了Struts2标签库;
- 第二行通过 value="username" 访问Action中的 getUsername() 方法;
- 第三行展示了嵌套对象属性访问;
- 第四行演示了字符串拼接运算。

代码逻辑逐行解读分析:
行号 代码片段 参数说明与执行逻辑
1 <%@ taglib prefix="s" uri="/struts-tags" %> 引入Struts2自定义标签库, prefix="s" 定义前缀别名,后续可用 <s:xxx> 调用标签。 uri 指向TLD文件位置,容器据此加载标签处理器类。
2 <p>用户名:<s:property value="username"/></p> <s:property> 标签输出指定值; value="username" 被解析为OGNL表达式,在ValueStack中查找名为 username 的属性(即调用Action的getter)。若存在则输出文本,否则为空。
3 <p>用户年龄:<s:property value="user.age"/></p> 使用点号导航访问嵌套对象属性,相当于调用 getUser().getAge() 。OGNL会自动处理空指针异常(默认返回null而非抛出异常)。
4 <p>欢迎信息:<s:property value="'Hello, ' + username"/></p> 支持表达式运算:左侧为字符串字面量,右侧为变量引用, + 执行连接操作。此为纯OGNL表达式能力体现,无需Java代码介入即可完成动态拼接。

该机制显著降低了视图层对脚本片段(如 <%= ... %> )的依赖,提高了安全性与维护性。

5.1.3 ValueStack双栈结构解析

Struts2的ValueStack并非单一栈结构,而是由两个部分组成: Object Stack(值栈) Context Map(上下文映射)

graph TD
    A[ValueStack] --> B[Object Stack]
    A --> C[Context Map]
    B --> D[Action实例]
    B --> E[ModelDriven模型对象]
    B --> F[其他压入对象]
    C --> G[#request]
    C --> H[#session]
    C --> I[#application]
    C --> J[#parameters]
    C --> K[#attr]
  • Object Stack :存放Action实例本身及其关联的对象(如通过ModelDriven绑定的领域模型)。当使用 value="name" 时,OGNL首先在此栈顶向下搜索匹配属性。
  • Context Map :封装Servlet API中的各种作用域对象,通过 # 前缀访问,如 #session.username 表示从HttpSession中获取 username

查找顺序遵循“就近优先”原则:
1. 先在Object Stack中按栈顶到底依次查找属性;
2. 若未找到,则在Context Map中查找;
3. 最终尝试解析为静态成员或方法。

理解这一结构有助于避免属性覆盖问题,提升调试效率。

5.1.4 OGNL表达式中的特殊符号与作用域控制

Struts2扩展了OGNL语法以增强灵活性,主要体现在以下几个特殊符号的使用:

符号 含义 示例 用途说明
# 访问Context Map中的全局变量 #session.user , #application.config 用于读取会话、应用级数据
%{} 强制OGNL求值 %{#session.count + 1} 在非标准属性环境中强制解析表达式
${} Web层面EL表达式兼容 ${username} 可被某些拦截器识别,但推荐统一使用OGNL
[] 数组/列表索引或Map键访问 users[0].name , map['key'] 支持动态键名访问

特别地, %{} 常用于需要明确表达式意图的场景。例如,在HTML属性中混合使用静态与动态内容:

<input type="text" name="email" value="%{user.email}" />

此处 %{} 明确告诉Struts2引擎: user.email 应作为OGNL表达式求值,而非普通字符串。

5.1.5 安全性考虑与表达式沙箱机制

尽管OGNL功能强大,但也带来了潜在的安全风险,尤其是在允许用户输入影响表达式内容的情况下。历史上曾发生过因OGNL表达式注入导致远程代码执行(RCE)的重大漏洞(如CVE-2010-1870)。

Struts2为此引入了 安全表达式限制机制 ,包括:

  • 默认禁用静态方法调用(可通过 struts.ognl.allowStaticMethodAccess=false 控制);
  • 对敏感类(如 Runtime , Class )进行黑名单过滤;
  • 提供 ParameterInterceptor 过滤危险参数名称(如包含 class method: 等关键字);

建议生产环境始终关闭静态方法访问权限,并定期更新Struts2版本以修复已知漏洞。

5.1.6 性能优化建议与缓存机制

由于每次JSP渲染都会频繁解析OGNL表达式,因此性能成为关键考量因素。Struts2内部采用了 表达式编译缓存机制 ,即将常用表达式编译为字节码并缓存,避免重复解析。

开发者应遵循以下最佳实践以提升性能:

  • 避免在循环内使用复杂表达式;
  • 尽量减少嵌套层级过深的对象访问;
  • 使用 var 属性缓存中间结果,如:
<s:set var="currentUser" value="#session.user"/>
<p>姓名:<s:property value="#currentUser.name"/></p>
<p>角色:<s:property value="#currentUser.role.name"/></p>

通过 <s:set> 将频繁访问的对象缓存至局部变量,减少多次上下文查找开销。

5.2 使用OGNL进行复杂数据结构的渲染

5.2.1 列表遍历与 <s:iterator> 标签应用

在实际开发中,常需展示用户列表、订单记录等集合数据。Struts2通过 <s:iterator> 标签结合OGNL实现高效的集合迭代。

假设Action中有如下代码:

public class UserListAction extends ActionSupport {
    private List<User> users;

    public String execute() {
        users = userService.findAll(); // 获取用户列表
        return SUCCESS;
    }

    // getter/setter
    public List<User> getUsers() { return users; }
}

对应的JSP页面可编写为:

<table border="1">
    <tr><th>ID</th><th>姓名</th><th>邮箱</th></tr>
    <s:iterator value="users" var="user" status="stat">
        <tr class="<s:if test="#stat.even">even</s:if><s:else>odd</s:else>">
            <td><s:property value="#stat.index + 1"/></td>
            <td><s:property value="name"/></td>
            <td><s:property value="email"/></td>
        </tr>
    </s:iterator>
</table>
代码块解释与参数说明:
<s:iterator value="users" var="user" status="stat">
  • value="users" :OGNL表达式,指向Action中的 getUsers() 返回的List;
  • var="user" :将当前迭代元素绑定到局部变量 user ,可在后续标签中通过 #user 引用;
  • status="stat" :创建一个 IteratorStatus 对象,提供索引、奇偶行判断等功能。
<tr class="<s:if test="#stat.even">even</s:if><s:else>odd</s:else>">
  • #stat.even :调用 IteratorStatus.isEven() 方法,返回布尔值;
  • <s:if> 根据条件决定输出哪个CSS类名,实现隔行变色效果。
<td><s:property value="#stat.index + 1"/></td>
  • #stat.index :从0开始的当前索引;
  • + 1 实现序号从1开始显示。

该模式广泛应用于表格、菜单、分页等场景,具有高度复用性。

5.2.2 条件判断与 <s:if> <s:elseif> <s:else> 组合使用

Struts2提供了一套完整的条件控制标签体系,底层基于OGNL布尔表达式求值。

示例:根据用户权限显示不同按钮

<s:if test="role == 'admin'">
    <button onclick="deleteUser()">删除用户</button>
</s:if>
<s:elseif test="role == 'editor'">
    <button onclick="editContent()">编辑内容</button>
</s:elseif>
<s:else>
    <span>只读用户</span>
</s:else>

其中 test 属性接收OGNL表达式,返回true/false决定是否渲染标签体内容。

更复杂的条件也可支持:

<s:if test="age >= 18 && (hasLicense == true or #session.vip == true)">
    允许驾驶
</s:if>

注意:OGNL中布尔值比较应使用 == true 而非省略,以防类型转换歧义。

5.2.3 投影与选择表达式在集合操作中的高级用法

OGNL支持类似函数式编程的集合操作:

操作类型 语法 示例 说明
投影(Projection) collection.{expression} users.{name} 提取所有用户的姓名,返回List
选择(Selection) collection.{? expression} users.{? #this.age > 18} 筛选成年用户
第一个匹配 collection.{^ expression} users.{^ #this.active} 返回首个激活用户
最后一个匹配 collection.{$ expression} logs.{$ #this.level == 'ERROR'} 返回最后一条错误日志

应用场景举例:

<!-- 显示所有用户名 -->
<ul>
<s:iterator value="users.{name}">
    <li><s:property/></li>
</s:iterator>
</ul>

<!-- 统计成年人数 -->
<p>成年人数:<s:property value="users.{? #this.age > 18}.size()"/></p>

此类表达式极大简化了视图层的数据处理逻辑,减少了对Action层预计算的依赖。

5.2.4 Map数据的访问与动态键名处理

对于Map类型数据,OGNL支持通过键名直接访问:

private Map<String, String> preferences;
// getter...

JSP中可写为:

<s:property value="preferences['theme']"/>
<s:property value="preferences.theme"/> <!-- 点号也可用 -->

动态键名可通过变量传入:

<s:set var="key" value="'language'"/>
<s:property value="preferences[#key]"/>

此外,还可结合 <s:iterator> 遍历Map:

<s:iterator value="preferences" var="entry">
    Key: <s:property value="#entry.key"/>, 
    Value: <s:property value="#entry.value"/><br/>
</s:iterator>

此时 var 接收的是 Map.Entry 对象,可通过 .key .value 访问。

5.2.5 方法调用与动态行为触发

OGNL允许在表达式中调用无参方法:

<p>系统时间:<s:property value="@java.util.Calendar@getInstance().time"/></p>
<p>格式化日期:<s:property value="formatDate(birthDate)"/></p>

前者调用静态工厂方法获取当前时间;后者调用Action中的实例方法 formatDate(Date d)

⚠️ 注意:方法调用应在只读场景下使用,避免产生副作用(如修改状态、保存数据库等)。

5.2.6 国际化消息与资源文件联动

结合Struts2的i18n拦截器,可使用OGNL访问资源包:

# messages_zh_CN.properties
welcome.message=欢迎,{0}!
button.save=保存

JSP中调用:

<s:text name="welcome.message">
    <s:param value="username"/>
</s:text>
<button><s:text name="button.save"/></button>

或直接在表达式中引用:

<s:property value="getText('button.edit')"/>

getText() 是ActionSupport提供的方法,支持参数替换与区域适配。

5.3 OGNL与标签库深度整合案例分析

5.3.1 表单数据回显与 <s:form> 自动填充

Struts2的 <s:form> 标签能自动从ValueStack中提取同名属性值并填充表单字段。

<s:form action="updateUser">
    <s:textfield name="username" label="用户名"/>
    <s:password name="password" label="密码"/>
    <s:select name="gender" list="{'男','女'}" label="性别"/>
    <s:submit value="提交"/>
</s:form>

当Action中存在 getUsername() 时,文本框将自动显示其值,实现“回显”。

原理: <s:textfield> 内部调用 findValue("username") 从ValueStack查找值并设置 value 属性。

5.3.2 下拉框动态生成与 <s:select> 的OGNL驱动

使用OGNL可动态生成选项:

private List<Role> roles;
private Integer selectedRoleId;
<s:select 
    name="selectedRoleId" 
    list="roles" 
    listKey="id" 
    listValue="name" 
    headerKey="-1" 
    headerValue="请选择角色"/>
  • list="roles" :OGNL表达式获取角色列表;
  • listKey="id" :每个选项的value属性;
  • listValue="name" :显示文本;
  • 自动生成 <option value="...">...</option>

5.3.3 复选框组与 <s:checkboxlist> 的多值绑定

适用于多项选择场景:

private String[] hobbies;
private List<String> hobbyList = Arrays.asList("读书", "运动", "音乐", "旅行");
<s:checkboxlist name="hobbies" list="hobbyList"/>

提交时自动绑定选中项至 hobbies 数组。

5.3.4 错误信息展示与 <s:actionerror> <s:fielderror>

验证失败后,错误信息存入ValueStack:

public void validate() {
    if (username == null || username.trim().isEmpty()) {
        addFieldError("username", "用户名不能为空");
    }
}

JSP中显示:

<s:fielderror fieldName="username" cssClass="error"/>
<s:actionerror/>

底层通过OGNL访问 _fieldErrors _actionErrors 集合。

5.3.5 自定义OGNL函数注册与扩展

可通过 OgnlRuntime 注册静态方法供全局使用:

// 工具类
public class ViewUtils {
    public static String truncate(String text, int len) {
        return text.length() > len ? text.substring(0, len) + "..." : text;
    }
}

配置 struts.properties

struts.ognl.allowStaticMethodAccess=true

JSP中使用:

<s:property value="@com.example.ViewUtils@truncate(description, 50)"/>

谨慎启用此功能,确保无安全风险。

5.3.6 调试技巧与表达式求值日志输出

开启开发模式可在控制台查看OGNL求值过程:

<constant name="struts.devMode" value="true"/>

访问页面时,Struts2会打印类似日志:

DEBUG ognl.OgnlContext - Getting value for expression 'users.size()'
DEBUG ognl.Ognl - Resolved [users] to List in ObjectStack

配合浏览器插件(如Struts2 Inspector),可实时查看ValueStack内容。


综上所述,OGNL不仅是Struts2数据传递的核心技术,更是连接控制器与视图的强大桥梁。掌握其语法、机制与最佳实践,能够大幅提升开发效率与系统健壮性。后续章节将进一步探讨Result类型配置,实现完整的请求-响应闭环。

6. Result结果类型配置与页面跳转逻辑

Struts2框架在MVC架构中承担着核心的控制器职责,其灵活性不仅体现在请求处理流程的设计上,更在于对响应输出方式的多样化支持。Result结果类型机制正是实现这一能力的关键组成部分。通过合理配置不同的 Result 类型,开发者可以精确控制Action执行完毕后系统的行为路径——无论是跳转至JSP页面、重定向到另一个Action、链式调用内部方法,还是返回纯文本或JSON数据用于前后端分离场景,均能通过声明式配置完成。

本章将深入剖析Struts2内置的主要结果类型,从底层执行机制到实际应用场景进行逐层解析,并结合代码示例、流程图和配置表格说明每种类型的适用边界与性能影响。特别关注 dispatcher redirect 之间的本质区别,探讨何时应选择 redirectAction 避免表单重复提交问题,以及如何利用 chain 实现跨Action的数据传递。此外,还将介绍基于插件扩展的高级用法,如集成JSON Result生成RESTful接口,甚至自定义Result类以支持PDF、Excel等特殊格式输出。

6.1 内置Result类型详解及其语义差异

Struts2提供了多种预定义的结果类型(result types),它们被注册在 struts-default.xml 中,作为全局可用的基础组件。这些类型决定了Action执行完成后请求流转的方式和响应内容的组织形式。理解各类Result的行为特征对于构建健壮且用户体验良好的Web应用至关重要。

6.1.1 dispatcher结果类型:服务器内部转发

dispatcher 是Struts2中最常用的结果类型,默认情况下无需显式指定。它对应于Servlet规范中的 RequestDispatcher.forward() 操作,意味着请求在服务器端被“内部转移”到目标资源(通常是JSP页面),而客户端浏览器地址栏保持不变。

<action name="showUser" class="com.example.action.UserAction">
    <result type="dispatcher">/WEB-INF/pages/user.jsp</result>
</action>
参数说明:
  • type="dispatcher" :可省略,因它是默认值。
  • 路径为相对路径,推荐使用 /WEB-INF/ 下受保护目录防止直接访问。

该方式适用于需要保留当前请求上下文(包括ValueStack中的数据)并渲染视图的情况。由于未发生HTTP重定向,所有请求参数、属性均可直接传递给目标JSP。

执行逻辑分析:
flowchart TD
    A[用户发起请求 /showUser.action] --> B{StrutsPrepareAndExecuteFilter拦截}
    B --> C[执行拦截器栈]
    C --> D[调用UserAction.execute()]
    D --> E[返回success]
    E --> F[查找<result type='dispatcher'>]
    F --> G[RequestDispatcher.forward() 到 user.jsp]
    G --> H[渲染JSP,输出HTML]
    H --> I[响应返回客户端]

此流程图展示了 dispatcher 类型的核心流转过程:整个过程发生在一次HTTP请求内,无额外网络往返,效率高但无法刷新URL。

6.1.2 redirect结果类型:客户端重定向

dispatcher 不同, redirect 类型会向客户端发送一个 HTTP 302 状态码,指示浏览器重新发起对新URL的GET请求。这会导致地址栏更新,同时原始请求中的参数和属性丢失(除非手动附加)。

<action name="saveUser" class="com.example.action.UserAction" method="save">
    <result name="success" type="redirect">/listUsers.action</result>
</action>
参数说明:
  • type="redirect" :触发HttpServletResponse.sendRedirect()
  • 目标URL可以是绝对路径或相对于当前上下文根的路径

这种模式常用于防止“重复提交”问题。例如,在保存用户信息后重定向到列表页,即使用户刷新页面也不会再次触发保存操作。

代码逻辑逐行解读:
public String save() {
    userService.save(user); // 保存实体
    addActionMessage("用户添加成功!"); // 添加消息
    return SUCCESS;         // 返回success,触发redirect
}
  • 第1行:执行业务逻辑;
  • 第2行:尝试添加ActionMessage,但由于 redirect 导致ValueStack失效,普通消息不会自动携带;
  • 第3行:返回结果名,匹配 <result type="redirect">

⚠️ 注意:标准 redirect 不支持OGNL表达式求值,若需动态拼接参数,应使用 ${} 语法:

<result type="redirect">
    /viewUser.action?id=${user.id}
</result>

此时 ${user.id} 会在Result执行前通过OGNL解析为实际值。

6.1.3 redirectAction结果类型:重定向至其他Action

当目标不是静态资源而是另一个Action时, redirectAction 提供了更优雅的解决方案。它允许开发者指定目标Action名称及命名空间,避免硬编码URL。

<result type="redirectAction">
    <param name="actionName">listUsers</param>
    <param name="namespace">/admin</param>
    <param name="method">search</param>
</result>
参数说明表:
参数名 含义 是否必需
actionName 目标Action的name属性
namespace 目标命名空间 否(默认”/”)
method 指定调用的方法
id 可传递额外参数(如id=${userId})

该配置最终生成类似 /admin/listUsers!search.action?id=123 的URL并执行重定向。

流程图展示:
flowchart LR
    A[Action A 执行完成] --> B{Result type=redirectAction?}
    B -->|是| C[构建目标Action URL]
    C --> D[调用Servlet API sendRedirect()]
    D --> E[浏览器发起新请求]
    E --> F[Struts拦截新请求]
    F --> G[执行目标Action]

相比手动拼接字符串URL, redirectAction 更具可维护性,尤其在模块化项目中命名空间频繁变化时优势明显。

6.1.4 chain结果类型:Action链式调用

chain 是一种特殊的内部调用机制,允许当前Action执行结束后继续调用另一个Action,且共享同一个ValueStack,从而实现数据无缝传递。

<action name="createOrder" class="com.example.OrderAction" method="create">
    <result type="chain">
        <param name="actionName">sendNotification</param>
        <param name="namespace">/notification</param>
    </result>
</action>
特性分析:
  • 共享ActionContext与ValueStack;
  • 不涉及任何HTTP跳转,仍属于同一请求;
  • 可用于实现“创建订单 → 发送邮件通知”的业务流程编排;
局限性:
  • 若链过长可能导致调试困难;
  • 性能开销略高于单一Action;
  • 不适合跨模块复杂依赖;
示例代码配合说明:
public class SendNotificationAction extends ActionSupport {
    private Order order; // 自动从上一个Action继承

    public String execute() {
        System.out.println("发送通知给:" + order.getCustomerEmail());
        return NONE;
    }

    // getter/setter...
}

在此例中, OrderAction 创建的 order 对象会被自动注入到后续 SendNotificationAction 中,前提是两者在同一ValueStack作用域下。

6.1.5 plainText结果类型:返回原始文本内容

plainText 用于直接输出文件内容或字符串,常用于查看源码、返回脚本片段或简单API响应。

<action name="viewSource" class="com.example.SourceAction">
    <result type="plainText">
        <param name="location">/WEB-INF/actions/UserAction.java</param>
        <param name="charSet">UTF-8</param>
    </result>
</action>
支持参数:
  • location :要读取的资源路径;
  • charSet :字符编码;
  • contentCharSet :设置Content-Type头部编码;

✅ 应用场景:后台管理系统中提供“查看Action源码”功能,便于开发人员快速查阅。

6.2 多视图技术集成与模板引擎支持

Struts2不仅仅局限于JSP,还支持FreeMarker、Velocity等主流模板引擎,通过配置不同结果类型即可实现多视图共存。

6.2.1 使用FreeMarker作为视图模板

首先确保引入 struts2-freemarker-plugin 依赖:

<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-freemarker-plugin</artifactId>
    <version>2.5.30</version>
</dependency>

然后配置Action返回 .ftl 模板:

<action name="home" class="com.example.HomeAction">
    <result type="freemarker">/templates/home.ftl</result>
</action>
FreeMarker模板示例(home.ftl):
<!DOCTYPE html>
<html>
<head><title>首页</title></head>
<body>
<h1>欢迎 ${userName}!</h1>
<ul>
<#list recentOrders as order>
    <li>订单编号: ${order.id}, 金额: ${order.amount}</li>
</#list>
</ul>
</body>
</html>
优势对比表:
视图技术 编译方式 性能表现 OGNL兼容性 适用场景
JSP 运行时编译 中等 传统企业级应用
FreeMarker 预编译/缓存 内容管理系统、邮件模板
Velocity 解释执行 较低 老旧系统迁移

推荐优先采用FreeMarker提升渲染效率,特别是在高并发环境下。

6.2.2 JSON结果类型构建RESTful服务

随着前后端分离趋势加剧,Struts2通过 struts2-json-plugin 支持直接返回JSON数据。

Maven依赖:
<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-json-plugin</artifactId>
    <version>2.5.30</version>
</dependency>
Action配置:
<action name="getUserJson" class="com.example.UserAction" method="getJson">
    <result type="json">
        <param name="includeProperties">id,name,email,orders\[\].*</param>
    </result>
</action>
Action类代码:
public class UserAction extends ActionSupport {
    private User user;

    public String getJson() {
        user = userService.findById(id);
        return SUCCESS;
    }

    // getter/setter...
}
参数说明:
  • includeProperties :正则式过滤输出字段;
  • excludeProperties :排除敏感信息如密码;
  • root :指定序列化根对象(默认为Action本身);
输出示例:
{
  "id": 1,
  "name": "张三",
  "email": "zhangsan@example.com",
  "orders": [
    {"id": 101, "amount": 99.9},
    {"id": 102, "amount": 199.9}
  ]
}

💡 提示:结合Ajax前端框架(如Vue、React),可轻松搭建轻量级后端API服务。

6.3 自定义Result类型扩展机制

当内置类型无法满足需求时(如生成PDF报表、导出Excel文件),可通过实现 com.opensymphony.xwork2.Result 接口来自定义结果处理器。

6.3.1 定义自定义Result类

public class PdfResult implements Result {

    private String location;
    private String fileName;

    @Override
    public void execute(ActionInvocation invocation) throws Exception {
        HttpServletResponse response = ServletActionContext.getResponse();
        response.setContentType("application/pdf");
        response.setHeader("Content-Disposition", 
            "attachment; filename=\"" + fileName + ".pdf\"");

        Document document = new Document();
        PdfWriter writer = PdfWriter.getInstance(document, response.getOutputStream());
        document.open();
        document.add(new Paragraph("由Struts2生成的PDF文档"));
        Object action = invocation.getAction();
        if (action instanceof PdfCapable) {
            String content = ((PdfCapable) action).getPdfContent();
            document.add(new Paragraph(content));
        }
        document.close();
    }

    // getter/setter for location and fileName
}
接口契约说明:
  • execute(ActionInvocation) :核心执行方法;
  • invocation :持有当前Action实例与上下文环境;
  • 可访问HTTP响应流直接写入二进制内容;

6.3.2 注册并使用自定义Result

struts.xml 中注册新类型:

<package name="pdf-package" extends="struts-default">
    <result-types>
        <result-type name="pdf" class="com.example.result.PdfResult"/>
    </result-types>

    <action name="downloadReport" class="com.example.ReportAction">
        <result type="pdf">
            <param name="fileName">monthly_report</param>
        </result>
    </action>
</package>
功能延展建议:
  • 结合iText或Apache PDFBox增强样式;
  • 支持模板化PDF生成;
  • 添加水印、页眉页脚等高级特性;

6.3.3 实际应用场景举例:Excel导出

类似地,可创建 ExcelResult 类,利用POI库将List 导出为XLS文件:

HSSFWorkbook workbook = new HSSFWorkbook();
HSSFSheet sheet = workbook.createSheet("用户列表");
int rowIdx = 0;
for (User u : users) {
    HSSFRow row = sheet.createRow(rowIdx++);
    row.createCell(0).setCellValue(u.getId());
    row.createCell(1).setCellValue(u.getName());
    row.createCell(2).setCellValue(u.getEmail());
}
workbook.write(response.getOutputStream());

此类设计体现了Struts2的高度可扩展性,使框架不仅能胜任传统Web开发,也能适应现代微服务与报表系统的需求。


综上所述,Result类型不仅是Struts2实现灵活跳转的核心机制,更是连接业务逻辑与用户界面的重要桥梁。通过对不同类型的理解与组合运用,开发者能够精准掌控每一次请求的归宿,无论是在页面导航、数据交互还是文件生成层面,都能找到最优解。

7. Struts2入门项目完整搭建与运行实战

7.1 开发环境准备与项目结构初始化

在开始构建Struts2项目之前,首先需要确保开发环境的完备性。推荐使用以下技术栈组合:

  • JDK 8+ :Struts2基于Java SE平台,需安装并配置好JDK环境变量( JAVA_HOME )。
  • Apache Tomcat 9.x :作为Servlet容器,用于部署和运行Web应用。
  • IDE工具 :推荐使用 IntelliJ IDEA 或 Eclipse,支持Maven集成。
  • 构建工具 :采用 Maven 进行依赖管理,提升项目可维护性。

创建Maven Web项目结构如下:

src/
├── main/
│   ├── java/
│   │   └── com/example/action/HelloWorldAction.java
│   ├── resources/
│   │   ├── struts.xml
│   │   └── struts.properties
│   └── webapp/
│       ├── WEB-INF/
│       │   └── web.xml
│       └── hello.jsp
pom.xml

该目录结构符合标准Java Web项目的布局规范,便于后续打包与部署。

7.2 Maven依赖配置与核心库引入

pom.xml 中添加必要的Struts2核心依赖。以下是关键依赖项列表(不少于10行数据):

<dependencies>
    <!-- Struts2 核心框架 -->
    <dependency>
        <groupId>org.apache.struts</groupId>
        <artifactId>struts2-core</artifactId>
        <version>2.5.30</version>
    </dependency>

    <!-- OGNL 表达式语言支持(内嵌于struts2-core但可单独声明) -->
    <dependency>
        <groupId>ognl</groupId>
        <artifactId>ognl</artifactId>
        <version>3.3.4</version>
    </dependency>

    <!-- 字节码操作库,用于动态代理 -->
    <dependency>
        <groupId>org.javassist</groupId>
        <artifactId>javassist</artifactId>
        <version>3.29.2-GA</version>
    </dependency>

    <!-- Servlet API(provided范围,由Tomcat提供) -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>4.0.1</version>
        <scope>provided</scope>
    </dependency>

    <!-- JSP 标签库支持 -->
    <dependency>
        <groupId>org.apache.struts</groupId>
        <artifactId>struts2-convention-plugin</artifactId>
        <version>2.5.30</version>
    </dependency>
</dependencies>

上述依赖确保了Struts2运行所需的所有基础组件,包括OGNL解析、拦截器机制、结果类型处理等。

7.3 配置web.xml:注册前端控制器

WEB-INF/web.xml 中配置 StrutsPrepareAndExecuteFilter ,它是现代Struts2应用的入口点。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
                             http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!-- Struts2 前端控制器 -->
    <filter>
        <filter-name>struts2</filter-name>
        <filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>struts2</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- 设置默认页面 -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

此配置将所有请求交由Struts2处理,实现统一调度。

7.4 编写Action类与业务逻辑实现

创建 HelloWorldAction.java ,继承自POJO模式,无需强制实现接口:

package com.example.action;

import com.opensymphony.xwork2.ActionSupport;

public class HelloWorldAction extends ActionSupport {
    private String message;

    // execute 方法返回 success 触发跳转
    public String execute() {
        message = "Hello, Struts2 World!";
        return SUCCESS;
    }

    // getter/setter 供JSP访问
    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

SUCCESS 是父类 ActionSupport 定义的常量,值为 "success" ,用于结果映射。

7.5 struts.xml 配置文件编写

resources/struts.xml 中定义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="default" extends="struts-default" namespace="/">
        <action name="hello" class="com.example.action.HelloWorldAction">
            <result name="success">/hello.jsp</result>
        </action>
    </package>
</struts>

namespace="/" 表示URL根路径; extends="struts-default" 继承默认拦截器栈与结果类型。

7.6 JSP视图页面开发与OGNL数据展示

创建 hello.jsp 页面,使用Struts标签库输出Action传递的数据:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head><title>Hello Struts2</title></head>
<body>
    <h2><s:property value="message"/></h2>
    <p>当前时间:<s:property value="new java.util.Date()"/></p>
</body>
</html>

<s:property value="message"/> 利用OGNL从ValueStack中获取Action属性值。

7.7 项目部署与访问验证流程

执行以下步骤完成部署测试:

  1. 使用 mvn clean package 打包生成 .war 文件;
  2. 将WAR包复制到 Tomcat/webapps/ 目录;
  3. 启动Tomcat服务器;
  4. 浏览器访问: http://localhost:8080/your-app/hello.action

预期输出:“Hello, Struts2 World!” 及当前时间。

7.8 常见启动异常排查对照表

异常现象 可能原因 解决方案
ClassNotFoundException: StrutsPrepareAndExecuteFilter 缺少struts2-core依赖 检查Maven依赖是否正确导入
NoResultException 返回结果未配置或拼写错误 确认struts.xml中result name匹配execute返回值
页面显示空白 JSP标签库URI错误 使用正确的taglib URI /struts-tags
参数无法注入 getter/setter缺失 保证字段有公共setter方法
中文乱码 请求编码未设置 添加 <constant name="struts.i18n.encoding" value="UTF-8"/>
DevMode无日志 devMode未启用 在struts.xml中设置 struts.devMode=true
Action不执行 URL路径错误 检查namespace与action name拼写
OGNL表达式不解析 忘记引入struts-tags 确保JSP顶部已声明taglib
方法调用失败 动态方法调用被禁用 启用DMI或改用method属性指定
拦截器未生效 自定义栈未引用 显式在action中指定interceptor-ref

7.9 使用Mermaid绘制请求处理流程图

sequenceDiagram
    participant Client
    participant Filter as StrutsPrepareAndExecuteFilter
    participant Action
    participant Result
    participant JSP

    Client->>Filter: 发送 /hello.action 请求
    activate Filter
    Filter->>Action: 实例化 HelloWorldAction
    Action->>Action: 执行 execute() 方法
    Action-->>Filter: 返回 "success"
    Filter->>Result: 查找 result 映射
    Result->>JSP: 转发至 hello.jsp
    JSP-->>Client: 渲染响应内容
    deactivate Filter

该流程图清晰展示了从HTTP请求进入过滤器,到最终视图渲染的全过程,体现了Struts2的MVC流转机制。

7.10 优化建议与扩展方向

可通过以下方式进一步提升项目质量:
- 引入 struts2-rest-plugin 支持RESTful风格API;
- 使用 @Namespace @Action 注解替代XML配置(需开启Convention插件);
- 集成Spring实现IOC管理Action实例;
- 添加全局异常拦截器统一处理错误;
- 使用Log4j2记录运行时日志,便于生产环境监控。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:Struts2是Java Web开发中广泛应用的MVC框架,以其清晰的架构和强大的功能著称。本“struts入门最简单例子”项目专为初学者设计,通过一个精简但完整的Web应用实例,帮助开发者快速掌握Struts2的核心概念与基本配置。内容涵盖Action类的定义、struts.xml配置文件的使用、结果类型映射、JSP视图展示以及OGNL表达式在数据传递中的应用。项目结构清晰,便于理解MVC模式在实际开发中的实现方式,为后续学习拦截器、表单验证、文件上传等高级功能打下坚实基础。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值