1. 从Servlet到Struts:为什么我们需要一个“控制器”?
如果你是从Servlet和JSP时代一路走过来的Java Web开发者,听到“Struts”这个名字,大概率会勾起一些复杂的回忆。它可能代表着你的第一个MVC框架,也可能代表着一段与复杂XML配置“搏斗”的时光。今天,我们不谈情怀,只谈技术。我想从一个最根本的问题开始:在Servlet/JSP已经能完成所有工作的年代,我们为什么还需要Struts?
让我们回到那个典型的Servlet开发场景。一个用户提交登录表单,请求发送到LoginServlet。在这个Servlet的doPost方法里,你需要做以下几件事:
- 调用
request.getParameter()获取用户名和密码。 - 连接数据库,进行校验。
- 根据校验结果,决定下一步:
- 成功:将用户对象存入Session,然后
request.getRequestDispatcher(“/welcome.jsp”).forward(request, response); - 失败:在Request中设置一个错误信息属性,然后
request.getRequestDispatcher(“/login.jsp”).forward(request, response);
- 成功:将用户对象存入Session,然后
看起来清晰明了,对吧?但问题会随着项目规模膨胀而指数级增长。想象一下,一个中等规模的系统有50个功能模块,对应50个Servlet。你会发现:
- URL映射混乱:每个Servlet都需要在
web.xml里配置一个<servlet>和<servlet-mapping>。维护这个日益庞大的XML文件本身就是一场噩梦。 - 代码重复严重:每个Servlet里都有获取参数、类型转换、校验逻辑。这些样板代码(Boilerplate Code)占据了大量的行数。
- 视图控制耦合:Servlet直接决定了跳转到哪个JSP页面。如果有一天需要把
welcome.jsp改名为home.jsp,你必须找到所有跳转到它的Servlet进行修改。 - 团队协作困难:没有统一的流程规范,每个程序员写Servlet的风格都可能不同,导致代码可读性和可维护性急剧下降。
Struts 1.x 的出现,就是为了解决这些痛点。它的核心思想是“关注点分离”和“约定优于配置”(虽然它的配置也不少)。它将上述Servlet中的流程标准化、模块化了:
- 用一个统一的“前端控制器”(ActionServlet)接收所有请求,替代了分散的Servlet。
web.xml里只需要配置这一个Servlet。 - 用“动作”(Action)类来处理具体业务逻辑,它只负责调用Service、准备数据,而不关心下一步跳转到哪里。
- 用“配置文件”(struts-config.xml)来管理请求路径、Action类、跳转页面之间的映射关系。修改页面路径只需改配置文件,无需动Java代码。
- 用“表单Bean”(ActionForm)来封装请求参数,自动完成类型转换和数据校验,将开发者从繁琐的
getParameter中解放出来。
所以,Struts的本质是一个基于MVC设计模式的请求驱动型Web框架。它通过引入一个中心化的控制层,为混乱的Web开发流程建立起了秩序。尽管它如今已被更现代的框架(如Spring MVC)所取代,但理解Struts的运作机制,尤其是其访问流程和配置文件,是理解Java Web MVC框架演进史的绝佳起点。接下来,我们就亲手搭建一个环境,看看这套机制是如何运转起来的。
2. 快速入门:亲手搭建你的第一个Struts应用
理论说再多,不如动手跑一遍。我们用一个最经典的例子——“用户登录”,来快速感受Struts的完整流程。我会假设你使用Eclipse或IDEA,并已配置好Tomcat服务器。
2.1 环境准备与项目创建
首先,你需要准备Struts的核心库文件。访问Apache官网(注意:Struts 1已停止维护,但学习资料和库文件仍可找到),下载Struts 1.x(例如1.3.10)的发行包。解压后,我们主要关注lib文件夹下的JAR包。对于一个基础应用,你至少需要以下JAR文件:
struts-core-1.3.10.jar:Struts框架的核心类库。commons-beanutils-1.8.3.jar:Bean工具库,用于操作JavaBean。commons-digester-2.1.jar:XML解析工具,用于读取struts-config.xml。commons-logging-1.1.3.jar:通用日志接口。commons-validator-1.3.1.jar:校验框架。 将这些JAR包复制到你的Web项目的WEB-INF/lib目录下。
2.2 核心四步:从请求到响应的完整链条
Struts应用的骨架由四个核心部分组成,我们一步步来创建。
第一步:配置前端控制器(web.xml)所有的请求都需要先经过Struts的“总闸”——ActionServlet。在WEB-INF/web.xml中配置它。
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <!-- 定义Struts的核心控制器Servlet --> <servlet> <servlet-name>action</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <!-- 指定Struts配置文件的路径,默认为/WEB-INF/struts-config.xml --> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <!-- 将所有以.do结尾的请求映射到ActionServlet --> <servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> <!-- 设置欢迎页面 --> <welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list> </web-app>这里的关键点是<url-pattern>*.do</url-pattern>。它意味着所有以.do结尾的URL都会被ActionServlet拦截处理。这是一种约定,你可以用其他模式,如/action/*。
第二步:创建视图(JSP页面)创建login.jsp,这是一个简单的登录表单。
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Struts快速入门 - 登录</title> </head> <body> <h2>用户登录</h2> <%-- 注意:表单的action路径以 .do 结尾,对应web.xml中的映射 --%> <form action="login.do" method="post"> 用户名:<input type="text" name="username"><br> 密码:<input type="password" name="password"><br> <input type="submit" value="登录"> </form> <%-- 显示Action传递过来的错误信息 --%> <font color="red">${error}</font> </body> </html>同时,创建一个登录成功后的页面welcome.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>欢迎页</title> </head> <body> <h1>欢迎您,${username}!</h1> </body> </html>第三步:定义表单Bean与动作类(ActionForm & Action)这是Struts的“模型”和“控制器”部分。
- 创建ActionForm(LoginForm.java):用于封装表单数据,自动进行类型转换和基础校验。
package com.example.form; import org.apache.struts.action.ActionForm; public class LoginForm extends ActionForm { private String username; private String password; // 必须提供getter和setter方法,Struts通过反射调用它们 public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } // 可选:重置方法,在复用ActionForm实例时被调用以清空旧数据 public void reset() { this.username = null; this.password = null; } // 可选:表单验证方法,在Action.execute()之前被调用 public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) { ActionErrors errors = new ActionErrors(); if (username == null || username.trim().isEmpty()) { errors.add("username", new ActionMessage("error.username.required")); } if (password == null || password.trim().isEmpty()) { errors.add("password", new ActionMessage("error.password.required")); } return errors; } }- 创建Action(LoginAction.java):处理核心业务逻辑。
package com.example.action; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.struts.action.Action; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import com.example.form.LoginForm; public class LoginAction extends Action { @Override public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { // 1. 将通用的ActionForm向下转型为具体的LoginForm LoginForm loginForm = (LoginForm) form; String username = loginForm.getUsername(); String password = loginForm.getPassword(); // 2. 模拟业务逻辑处理(这里简化成硬编码校验) ActionForward forward = null; if ("admin".equals(username) && "123456".equals(password)) { // 登录成功 // 将数据存入request范围,供JSP页面显示 request.setAttribute("username", username); // 通过mapping.findForward找到配置文件中名为“success”的跳转定义 forward = mapping.findForward("success"); } else { // 登录失败 request.setAttribute("error", "用户名或密码错误!"); // 跳转回登录页面 forward = mapping.findForward("failure"); } // 3. 返回ActionForward对象,告诉Struts下一步去哪 return forward; } }第四步:编写中枢配置文件(struts-config.xml)这是Struts的“路由表”,将所有部分连接起来。在WEB-INF/下创建struts-config.xml。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.3//EN" "http://struts.apache.org/dtds/struts-config_1_3.dtd"> <struts-config> <!-- ========== 表单Bean定义 ========== --> <form-beans> <!-- 定义一个表单Bean,名为“loginForm”,对应我们写的LoginForm类 --> <form-bean name="loginForm" type="com.example.form.LoginForm"/> </form-beans> <!-- ========== 全局异常和全局转向定义(本例暂不涉及) ========== --> <!-- ========== 动作映射定义:核心中的核心 ========== --> <action-mappings> <!-- 定义一个动作映射 --> <action path="/login" <!-- 请求路径,对应login.do --> type="com.example.action.LoginAction" <!-- 处理动作类 --> name="loginForm" <!-- 使用的表单Bean名称 --> scope="request" <!-- 表单Bean的作用域:request或session --> validate="true" <!-- 是否调用ActionForm的validate方法 --> input="/login.jsp"> <!-- 如果校验失败,跳转回哪个页面 --> <!-- 定义局部转发(Forward) --> <forward name="success" path="/welcome.jsp"/> <forward name="failure" path="/login.jsp"/> </action> </action-mappings> <!-- ========== 消息资源文件定义(本例暂不涉及) ========== --> <!-- <message-resources parameter="com.example.ApplicationResources"/> --> </struts-config>现在,启动你的Tomcat服务器,访问http://localhost:8080/你的项目名/。你会看到登录页面,输入admin和123456,点击登录,就能看到欢迎页面。一个最基础的Struts应用就跑通了。这个过程看似步骤繁多,但每一步都职责清晰,构成了Struts MVC的基石。
3. 庖丁解牛:Struts请求访问流程深度解析
在点击“登录”按钮后,到看到“欢迎页”之前,Struts框架内部到底发生了什么?理解这个流程,是掌握Struts乃至任何MVC框架的关键。我们可以将整个过程拆解为一条清晰的“流水线”。
3.1 核心流程图与阶段拆解
整个流程可以概括为以下阶段,由ActionServlet这个总调度员严格控制:
用户请求 (login.do) ↓ ActionServlet (前端控制器) ↓ 1. 根据请求路径 `/login` 查找对应的 `<action>` 配置 ↓ 2. 根据 `name=”loginForm”` 创建/获取 LoginForm 实例,并用请求参数填充它 (populate) ↓ 3. 如果 `validate=”true”`,调用 LoginForm.validate() 进行表单校验 ↓ ┌─ 校验失败 ──→ 跳转到 `<action>` 中 `input=”/login.jsp”` 指定的页面 ↓ (校验成功) 4. 将 LoginForm 和请求对象等传递给 LoginAction.execute() 方法 ↓ 5. LoginAction 执行业务逻辑,并返回一个 ActionForward 对象 (如 `mapping.findForward(“success”)`) ↓ 6. ActionServlet 根据 ActionForward 对象,找到配置中对应的 `<forward name=”success” path=”/welcome.jsp”/>` ↓ 7. 使用 RequestDispatcher 转发 (forward) 到 `/welcome.jsp` ↓ JSP 页面渲染,响应返回给用户3.2 关键环节的技术内幕
1. ActionServlet的初始化与请求分发:在Web容器(如Tomcat)启动时,由于web.xml中配置了<load-on-startup>1</load-on-startup>,ActionServlet的init()方法会被立即调用。在这个方法中,它会解析struts-config.xml文件,将所有的配置信息(<form-bean>,<action-mapping>等)加载到内存中,形成一系列ModuleConfig对象。当请求到达时(如login.do),ActionServlet的doGet()或doPost()方法会被调用。它首先从请求路径中提取出动作路径(例如从/app/login.do中提取出/login),然后在其内存配置中查找匹配的<action>映射。
2. ActionForm的生命周期与数据填充:这是Struts的一个精巧设计,也是早期容易产生困惑的地方。ActionForm本质上是一个值对象(Value Object)或数据传输对象(DTO)。框架通过Java的反射机制,根据<action>中name属性指定的名称,找到对应的<form-bean>定义,进而实例化LoginForm类。接着,它会遍历LoginForm中所有的setter方法(如setUsername),根据setter方法名推导出请求参数名(username),然后调用request.getParameter(“username”)获取值,并调用setUsername(String)将其注入。这个过程称为“填充”(Population)。scope属性决定了这个ActionForm实例是放在HttpServletRequest还是HttpSession中。request范围意味着每次请求都会创建新的实例;session范围则会在会话中复用,此时reset()方法就非常重要,用于清除前一次请求的残留数据。
3. validate()方法的执行与校验流程:如果<action>配置中validate=”true”,在填充数据之后、执行Action之前,框架会自动调用ActionForm的validate()方法。开发者可以在这个方法里编写业务规则校验(如非空、格式、长度等)。validate()方法返回一个ActionErrors对象。如果这个对象为null或者不包含任何错误信息,流程继续;否则,流程将中断,直接跳转到input属性指定的页面(通常是表单页),并且ActionErrors中的信息可以通过<html:errors>标签在JSP上显示。这是一种声明式校验,将校验逻辑从Action中剥离,使得Action更专注于核心业务。
4. Action的execute()与请求转发:经过以上步骤,一个“干净”的、已校验的ActionForm对象,连同ActionMapping(封装了当前action的配置信息)、HttpServletRequest和HttpServletResponse,一起被传递给Action.execute()方法。这里是开发者编写业务逻辑的核心地带。业务逻辑执行完毕后,Action需要返回一个ActionForward对象,告诉框架“接下来去哪”。通常我们通过mapping.findForward(String name)来获取在配置文件中预定义的转发。ActionServlet拿到这个ActionForward后,会从中解析出真正的资源路径(如/welcome.jsp),然后使用RequestDispatcher.forward()进行服务器端跳转。至此,控制权从Struts框架交还给了Web容器,由容器来渲染最终的JSP页面。
注意:这里有一个非常重要的细节:Struts默认使用的是服务器端转发(Forward),而非重定向(Redirect)。转发是在一次请求内部完成的,因此
request范围内的属性(如我们设置的username)可以传递到JSP页面。而重定向是两次独立的请求,request范围内的数据会丢失。在Struts配置中,可以通过在<forward>标签中设置redirect=”true”来指定使用重定向,但这需要根据数据传递的需求谨慎使用。
理解了这个流程,你就掌握了Struts框架的“任督二脉”。无论配置看起来多复杂,都是在这个标准流程上的定制和扩展。接下来,我们就深入这个流程的“指挥中心”——struts-config.xml配置文件。
4. struts-config.xml配置文件:框架的“指挥中心”详解
如果说ActionServlet是Struts框架的“大脑”,那么struts-config.xml就是存储其全部“知识与指令”的“记忆库”。这个XML文件定义了整个应用的骨架和行为规则。它的结构清晰,但选项繁多,我们将其拆解为几个核心模块来逐一攻克。
4.1 全局配置:数据源、插件与异常处理
在文件的顶部,通常是一些影响整个应用的全局设置。
<struts-config> <!-- 数据源配置 (Struts 1.2.7+),用于定义全局数据库连接池 --> <data-sources> <data-source type="org.apache.commons.dbcp.BasicDataSource"> <set-property property="driverClassName" value="com.mysql.jdbc.Driver"/> <set-property property="url" value="jdbc:mysql://localhost:3306/mydb"/> <set-property property="username" value="root"/> <set-property property="password" value="password"/> <set-property property="maxActive" value="10"/> </data-source> </data-sources> <!-- 插件配置,用于在Struts启动和关闭时执行自定义逻辑 --> <plug-in className="com.example.plugin.CustomPlugin"> <set-property property="someConfig" value="someValue"/> </plug-in> <!-- 全局异常处理:定义某种异常类型发生时,统一跳转到哪个页面 --> <global-exceptions> <exception key="global.error.invalidlogin" <!-- 在资源文件中定义的错误消息key --> type="com.example.exception.AuthenticationException" path="/error.jsp" <!-- 发生此异常时跳转的路径 --> scope="request"/> </global-exceptions> <!-- 全局转发定义:所有Action都可以使用的公共跳转路径 --> <global-forwards> <forward name="home" path="/index.jsp"/> <forward name="error" path="/common/error.jsp"/> <forward name="logout" path="/logout.do" redirect="true"/> </global-forwards><data-sources>:在早期Java Web应用中,经常在这里配置数据库连接池。但在现代开发中,更推荐使用专门的连接池工具(如C3P0、Druid)或在Web容器中配置JNDI数据源。<plug-in>:这是Struts一个强大的扩展机制。你可以编写实现PlugIn接口的类,在应用启动时(init方法)加载一些全局资源(如缓存、配置),在应用关闭时(destroy方法)安全释放。Spring框架早期就提供了ContextLoaderPlugin,用于在Struts中集成Spring容器。<global-exceptions>和<global-forwards>:它们的作用范围是整个模块。<global-exceptions>实现了声明式的异常处理,将异常类型与处理页面解耦。<global-forwards>定义了公共的跳转目标,避免在多个<action>中重复定义相同的<forward>。
4.2 表单Bean定义:数据的第一道关卡
<form-beans>区域是所有ActionForm的注册中心。这里定义了表单数据的结构。
<form-beans> <!-- 动态表单Bean:无需编写Java类,通过配置文件定义字段和类型 --> <form-bean name="dynamicForm" type="org.apache.struts.action.DynaActionForm"> <form-property name="userId" type="java.lang.Integer"/> <form-property name="userName" type="java.lang.String"/> <form-property name="subscription" type="java.lang.Boolean" initial="false"/> </form-bean> <!-- 静态表单Bean:对应我们编写的LoginForm类 --> <form-bean name="loginForm" type="com.example.form.LoginForm"/> </form-beans>- 动态ActionForm(DynaActionForm):这是Struts提供的一个非常实用的特性。对于字段简单、无需复杂校验的表单,你不需要编写具体的
ActionForm类。只需在配置中声明字段名和类型,框架会在运行时动态生成表单对象。这极大地减少了编码量,是“约定优于配置”思想的体现。访问动态表单中的属性需要使用form.get(“propertyName”)方法。 - 静态ActionForm:就是继承
ActionForm的自定义类,提供了最强的类型安全和灵活性,可以编写复杂的validate()逻辑。
4.3 动作映射:请求路由的核心规则
<action-mappings>是配置文件的心脏,它定义了URL到处理逻辑的映射规则。每个<action>元素都是一条路由指令。
<action-mappings> <action path="/user/login" <!-- 【必填】请求路径标识符 --> type="com.example.action.LoginAction" <!-- 【必填】处理类 --> name="loginForm" <!-- 【可选】关联的表单Bean名称 --> scope="request" <!-- 【可选】表单Bean作用域,默认request --> validate="true" <!-- 【可选】是否校验,默认true --> input="/login.jsp" <!-- 【可选】校验失败或input转发目标 --> parameter="method" <!-- 【关键】用于动态方法调用 --> unknown="false" <!-- 【可选】是否作为默认action --> cancellable="false"> <!-- 【可选】是否启用“取消”功能 --> <!-- 局部转发定义,优先级高于全局转发 --> <forward name="success" path="/welcome.jsp"/> <forward name="failure" path="/login.jsp" redirect="false"/> <!-- 可以定义多个forward --> <forward name="needActivation" path="/activate.jsp"/> </action> <!-- 使用动态表单的Action示例 --> <action path="/user/register" type="com.example.action.RegisterAction" name="dynamicForm" <!-- 使用上面定义的动态表单 --> scope="request" validate="true" input="/register.jsp"> <forward name="success" path="/regSuccess.jsp"/> </action> <!-- 转发Action:不处理逻辑,仅用于页面跳转 --> <action path="/goToAbout" forward="/aboutus.jsp"/> </action-mappings>- 核心属性解析:
path:这是Action的“名字”,对应URL中的一部分(如/user/login.do)。它是查找Action映射的关键。type:负责处理请求的Action类。name与scope:这对属性共同决定了表单数据的生命周期。name指定使用的<form-bean>,scope决定这个Bean实例是存在于一次请求中(request)还是会话中(session)。对于多页表单(如向导式注册),使用session范围可以跨请求保存数据,但务必注意在最后清理它。validate与input:这是一对组合。当validate=”true”时,如果ActionForm.validate()返回错误,请求会自动跳转到input指定的路径。这是实现“表单提交->校验失败->回显表单”模式的标准化流程。parameter:这是实现动态方法调用(DMI)的关键属性,我们将在下一章详细展开。forward属性:如果<action>标签只有path和forward属性,没有type,那么这个Action就是一个“转发Action”。它不执行任何业务逻辑,仅仅是将请求转发到指定的资源(如JSP)。这常用于简化对纯展示页面的访问。
4.4 消息资源与控制器配置
文件的最后部分通常用于国际化等高级功能。
<!-- 消息资源文件:用于国际化(I18N)和集中管理提示信息 --> <message-resources parameter="com.example.ApplicationResources"/> <!-- 控制器配置:用于定制ActionServlet的行为 --> <controller processorClass="org.apache.struts.action.RequestProcessor" maxFileSize="10M" tempDir="/tmp/strutsuploads"/> </struts-config><message-resources>:这是Struts国际化的基石。它指向一个.properties文件(如ApplicationResources.properties或ApplicationResources_zh_CN.properties)。在JSP中,你可以使用<bean:message key=”welcome.title”/>来获取本地化的文本;在ActionForm.validate()中,可以使用ActionMessage来引用资源文件中的错误信息。这实现了界面文本与代码的分离。<controller>:用于替换默认的RequestProcessor或调整其行为。例如,你可以自定义一个Processor来处理文件上传的大小和临时目录(如示例所示),或者实现一些全局的预处理/后处理逻辑。
通过这样逐层拆解struts-config.xml,你会发现它就像一个精密的仪表盘,每一个开关和旋钮都控制着框架行为的某个方面。熟练掌握这个文件的配置,是成为Struts开发高手的必经之路。而其中最具技巧性的功能之一,就是接下来要讲的动态方法调用。
5. 动态方法调用:一个Action如何化身“多面手”?
在基础的Struts模式中,一个URL路径(/login.do)对应一个<action>配置,进而对应一个Action类的execute()方法。如果一个模块有多个相似操作(例如用户的增、删、改、查),我们就需要配置多个<action>,编写多个Action类,即使它们可能共享大部分逻辑。这显然不够优雅,会产生大量重复配置和类文件。动态方法调用(Dynamic Method Invocation, DMI)就是为了解决这个问题而生的。
5.1 DMI的原理与配置启用
DMI的核心思想是:让一个Action类不再只有一个固定的execute()入口,而是可以根据请求参数,动态地调用其内部的任意一个公有方法。
它的实现依赖于<action>标签中的一个关键属性:parameter。同时,请求URL中需要携带一个特定的参数来指明要调用的方法名。默认情况下,Struts 1.3为了安全是禁用DMI的,我们需要在struts-config.xml中显式启用它(通常通过配置控制器):
<controller> <!-- 关键配置:启用动态方法调用 --> <set-property property="config/${org.apache.struts.action.DISPATCHER_FROM_PARAMETER?}" value="true"/> </controller>更常见的做法是在struts-config.xml的<action>映射中直接利用parameter属性。其工作原理是:
- 在
<action>中定义parameter=”method”。 - 在请求URL中附加参数,如
/user.do?method=add。 - Struts框架会截取
method参数的值(这里是”add”)。 - 框架会尝试调用当前
Action类中名为add、且签名与execute方法相同(即返回ActionForward,参数相同)的公有方法。
5.2 实战:使用DMI重构用户管理模块
假设我们有一个UserAction,需要处理用户的增加、删除、修改、查询。不使用DMI时,我们需要配置四个action:/userAdd.do,/userDelete.do,/userUpdate.do,/userQuery.do,并可能对应四个Action类。使用DMI后,一切变得简洁。
第一步:编写多功能Action类(UserAction.java)
package com.example.action; import org.apache.struts.action.*; public class UserAction extends Action { // 默认入口,如果未指定method参数,则调用此方法 public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { return mapping.findForward(“menu”); // 比如跳转到用户管理菜单页 } // 新增用户 public ActionForward add(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { UserForm userForm = (UserForm) form; // 调用Service层,执行新增逻辑 userService.addUser(userForm); request.setAttribute(“message”, “用户添加成功!”); return mapping.findForward(“success”); } // 删除用户 public ActionForward delete(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { String userId = request.getParameter(“id”); userService.deleteUser(userId); request.setAttribute(“message”, “用户删除成功!”); return mapping.findForward(“success”); } // 更新用户 public ActionForward update(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { UserForm userForm = (UserForm) form; userService.updateUser(userForm); request.setAttribute(“message”, “用户信息已更新!”); return mapping.findForward(“success”); } // 查询用户 public ActionForward query(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { String name = request.getParameter(“name”); List<User> userList = userService.queryUsers(name); request.setAttribute(“userList”, userList); return mapping.findForward(“list”); } }第二步:简化struts-config.xml配置
<action-mappings> <!-- 一个Action配置,覆盖所有用户相关操作 --> <action path=”/user” type=”com.example.action.UserAction” name=”userForm” scope=”request” parameter=”method”> <!-- 关键:指定方法参数名为 method --> <forward name=”menu” path=”/userMenu.jsp”/> <forward name=”success” path=”/common/success.jsp”/> <forward name=”list” path=”/userList.jsp”/> <!-- input转发可用于所有方法的校验失败 --> <forward name=”input” path=”/userEdit.jsp”/> </action> </action-mappings>第三步:在JSP页面或链接中发起请求
<!-- 添加用户的链接 --> <a href=”user.do?method=add”>添加用户</a> <!-- 删除用户的表单,通过隐藏域传递method和方法参数 --> <form action=”user.do” method=”post”> <input type=”hidden” name=”method” value=”delete”/> <input type=”hidden” name=”id” value=”${user.id}”/> <input type=”submit” value=”删除”/> </form> <!-- 查询表单 --> <form action=”user.do” method=”get”> <input type=”hidden” name=”method” value=”query”/> 姓名:<input type=”text” name=”name”/> <input type=”submit” value=”查询”/> </form>5.3 DMI的优缺点与安全考量
优点:
- 配置极大简化:一个
<action>配置替代了多个,struts-config.xml文件变得非常清爽。 - 代码更内聚:相关功能集中在一个
Action类中,提高了代码的可维护性。 - 减少类爆炸:无需为每个简单操作创建单独的
Action类。
缺点与风险:
- 安全隐患(最重要!):DMI本质上是根据用户输入的参数值来反射调用方法。如果攻击者构造一个请求如
user.do?method=execute&someMaliciousParam=...,或者尝试调用类的其他公有方法(如getServlet),可能导致严重的安全漏洞。因此,在Struts 1.3及以后版本中,默认是禁用DMI的。 - 方法签名限制:被调用的方法必须具有与
execute完全相同的签名,这限制了灵活性。 - 调试复杂度增加:堆栈跟踪中的方法名是动态的,可能给调试带来一些不便。
安全实践建议:
- 谨慎评估:在安全性要求不高的内部系统,或经过严格输入过滤的情况下,可以考虑使用。
- 使用替代方案:更安全的做法是使用“DispatchAction”或其子类(如
LookupDispatchAction,MappingDispatchAction)。DispatchAction是Struts官方提供的、专门用于多方法处理的基类。它要求请求参数的值必须严格映射到Action类中预定义的方法名,并且框架内部有更严格的检查机制,比原始的DMI更安全。public class UserDispatchAction extends DispatchAction { public ActionForward add(...) { ... } public ActionForward delete(...) { ... } // 框架会确保只调用这些明确声明的方法 } - 输入过滤:如果必须使用DMI,务必在
Action基类或过滤器中,对method参数进行白名单校验,只允许调用预定的方法名。
动态方法调用体现了Struts框架在追求配置简洁和代码复用上所做的努力。它是一把双刃剑,用好了能大幅提升开发效率,用不好则会引入安全风险。理解其原理和边界,能帮助你在合适的场景做出恰当的选择。