Apache Struts 构建并运行示例应用指南

Apache Struts 6.12.0 构建并运行示例应用指南:archetype 生成工程,自定义 Action 与 JSP,war 打包排障,部署 Tomcat 9,登录校验与文件上传实测,内嵌 Jetty 运行与日志排查,全程实测。

Apache StrutsApache Struts2026/10/12

Apache Struts 构建并运行示例应用指南

目标:从零构建一个 Apache Struts 6.12.0 示例 Web 应用并在 Tomcat 9 上运行验证。示例应用由两部分组成:archetype 自带的 HelloWorld/Login 示例,以及本文新增的自定义 Action(hello 问候、upload 文件上传、goHello 重定向)。覆盖工程生成、代码编写、war 打包与排障、两种部署方式(复制 WAR 与 Manager 接口)、页面功能实测、内嵌 Jetty 运行与日志排查。所有命令与回显均在 Ubuntu 22.04.5 + Tomcat 9.0.58 环境实测通过。

一、环境信息

  • 操作系统:Ubuntu 22.04.5 LTS(jammy)
  • Java:OpenJDK 17.0.20.1
  • 构建工具:Apache Maven 3.6.3
  • Servlet 引擎:Apache Tomcat 9.0.58(systemd 托管,端口 8080,JVM 堆 -Xms256m -Xmx512m -Xmn128m)
  • Struts 版本:6.12.0
  • 工程目录:/root/struts2-demo
  • Tomcat Manager 账号:admin / q1w2e3r4(5.2 节配置)

示例应用组成:

模块来源说明
HelloWorld / Login / Menu / Register / Missingarchetype 自带登录校验、通配符、i18n 演示
hello(HelloAction + Hello.jsp)本文新增参数注入与 s:property / s:iterator 标签
upload(UploadAction + Upload.jsp)本文新增multipart 文件上传
goHello本文新增result type="redirect" 重定向演示

二、生成示例工程

2.1 生成命令

cd /root
mvn archetype:generate -B -DgroupId=com.example.struts -DartifactId=struts2-demo -Dversion=1.0.0 \
    -DarchetypeGroupId=org.apache.struts -DarchetypeArtifactId=struts2-archetype-blank \
    -DarchetypeVersion=6.0.0 -DinteractiveMode=false

实测回显(节选):

[INFO] Parameter: artifactId, Value: struts2-demo
[INFO] Parameter: version, Value: 1.0.0
[INFO] Parameter: package, Value: com.example.struts
[INFO] Project created from Archetype in dir: /root/struts2-demo
[INFO] BUILD SUCCESS
[INFO] Total time:  03:30 min

2.2 工程文件清单

cd /root/struts2-demo && find . -type f | sort

实测回显:

./pom.xml
./src/main/java/com/example/struts/example/ExampleSupport.java
./src/main/java/com/example/struts/example/HelloWorld.java
./src/main/java/com/example/struts/example/Login.java
./src/main/resources/com/example/struts/example/Login-validation.xml
./src/main/resources/com/example/struts/example/package.properties
./src/main/resources/com/example/struts/example/package_es.properties
./src/main/resources/example.xml
./src/main/resources/log4j2.xml
./src/main/resources/struts.xml
./src/main/webapp/WEB-INF/example/HelloWorld.jsp
./src/main/webapp/WEB-INF/example/Login.jsp
./src/main/webapp/WEB-INF/example/Menu.jsp
./src/main/webapp/WEB-INF/example/Missing.jsp
./src/main/webapp/WEB-INF/example/Register.jsp
./src/main/webapp/WEB-INF/example/Welcome.jsp
./src/main/webapp/WEB-INF/web.xml
./src/main/webapp/index.html
./src/test/java/com/example/struts/example/ConfigTest.java
./src/test/java/com/example/struts/example/HelloWorldTest.java
./src/test/java/com/example/struts/example/LoginTest.java

2.3 升级 Struts 版本到 6.12.0

archetype 生成的 pom 中 struts2.version 为 6.1.1,手工改为 6.12.0。修改后实测内容:

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <struts2.version>6.12.0</struts2.version>
    <log4j2.version>2.19.0</log4j2.version>
</properties>

2.4 archetype 自带示例说明

自带示例的核心配置在 src/main/resources/example.xml(原样全文):

<struts>
    <package name="example" namespace="/example" extends="struts-default">
        <default-action-ref name="HelloWorld" />
        <action name="HelloWorld" class="com.example.struts.example.HelloWorld">
            <result>/WEB-INF/example/HelloWorld.jsp</result>
        </action>
        <action name="Login_*" method="{1}" class="com.example.struts.example.Login">
            <result name="input">/WEB-INF/example/Login.jsp</result>
            <result type="redirectAction">Menu</result>
        </action>
        <action name="*" class="com.example.struts.example.ExampleSupport">
            <result>/WEB-INF/example/{1}.jsp</result>
        </action>
    </package>
</struts>

要点:

  • Login_* 通配符:Login_input.action 映射到 Login.input()、Login_execute.action 映射到 execute();
  • Login-validation.xml 对 username/password 做 requiredstring 校验(校验失败回到 input);
  • * 兜底动作把未知请求映射到同名 JSP(如 Menu → Menu.jsp)。

自带国际化资源 src/main/resources/com/example/struts/example/package.properties(原样节选):

HelloWorld.message = Struts is up and running...
requiredstring=  ${getText(fieldName)} is required.
password = Password
username = User Name
Missing.message =  This feature is under construction. Please try again in the next iteration.

对应西班牙语资源 package_es.properties(原样节选):

HelloWorld.message = \u00A1Struts est\u00E1 bien\!...
password = Contrase\u00F1a
username = Nombre de Usuario

自带 Action 与基类(节选):

public class HelloWorld extends ExampleSupport {
    public String execute() throws Exception {
        setMessage(getText(MESSAGE));
        return SUCCESS;
    }
    public static final String MESSAGE = "HelloWorld.message";
    private String message;
    public String getMessage() { return message; }
    public void setMessage(String message) { this.message = message; }
}
public class ExampleSupport extends ActionSupport {
}
public class Login extends ExampleSupport {
    public String execute() throws Exception {
        if (isInvalid(getUsername())) return INPUT;
        if (isInvalid(getPassword())) return INPUT;
        return SUCCESS;
    }
}

三、新增自定义 Action 与页面

3.1 HelloAction.java

路径:src/main/java/com/example/struts/example/HelloAction.java(全文):

package com.example.struts.example;

import com.opensymphony.xwork2.ActionSupport;
import java.util.ArrayList;
import java.util.List;

public class HelloAction extends ActionSupport {

    private String name;
    private List<String> features = new ArrayList<>();

    @Override
    public String execute() throws Exception {
        if (name == null || name.trim().isEmpty()) {
            name = "World";
        }
        features.add("Apache Struts 6.12.0");
        features.add("Apache Tomcat 9.0.58");
        features.add("OpenJDK 17.0.20.1");
        return SUCCESS;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<String> getFeatures() {
        return features;
    }
}

3.2 Hello.jsp

路径:src/main/webapp/WEB-INF/example/Hello.jsp(全文):

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
    <title>Hello Struts</title>
</head>

<body>
<h2>Hello, <s:property value="name"/>! This page is rendered by Struts tags.</h2>

<h3>Environment features</h3>
<ul>
    <s:iterator value="features">
        <li><s:property/></li>
    </s:iterator>
</ul>

</body>
</html>

3.3 UploadAction.java

路径:src/main/java/com/example/struts/example/UploadAction.java(全文):

package com.example.struts.example;

import com.opensymphony.xwork2.ActionSupport;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class UploadAction extends ActionSupport {

    private File upload;
    private String uploadContentType;
    private String uploadFileName;
    private String message;

    @Override
    public String execute() throws Exception {
        if (upload == null) {
            return INPUT;
        }
        File dest = new File("/tmp", uploadFileName);
        FileInputStream in = new FileInputStream(upload);
        FileOutputStream out = new FileOutputStream(dest);
        byte[] buf = new byte[8192];
        int n;
        while ((n = in.read(buf)) > 0) {
            out.write(buf, 0, n);
        }
        in.close();
        out.close();
        message = "Saved " + uploadFileName + " (" + upload.length() + " bytes, content-type=" + uploadContentType + ") to /tmp/" + uploadFileName;
        return SUCCESS;
    }

    public File getUpload() { return upload; }
    public void setUpload(File upload) { this.upload = upload; }
    public String getUploadContentType() { return uploadContentType; }
    public void setUploadContentType(String uploadContentType) { this.uploadContentType = uploadContentType; }
    public String getUploadFileName() { return uploadFileName; }
    public void setUploadFileName(String uploadFileName) { this.uploadFileName = uploadFileName; }
    public String getMessage() { return message; }
}

3.4 Upload.jsp

路径:src/main/webapp/WEB-INF/example/Upload.jsp(全文):

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head><title>Upload Demo</title></head>
<body>
<h2>Struts multipart upload demo</h2>
<s:form action="upload" method="post" enctype="multipart/form-data">
    <s:file name="upload" label="Select file"/>
    <s:submit value="Upload"/>
</s:form>
<s:actionerror/>
<p><s:property value="message"/></p>
</body>
</html>

3.5 struts.xml 最终版

路径:src/main/resources/struts.xml(全文,实测最终版):

<?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.enable.DynamicMethodInvocation" value="false"/>
    <constant name="struts.devMode" value="true"/>
    <constant name="struts.action.extension" value="do,action,"/>
    <constant name="struts.multipart.maxSize" value="10485760"/>

    <include file="example.xml"/>

    <package name="default" namespace="/" extends="struts-default">

        <default-action-ref name="index"/>

        <action name="index">
            <result type="redirectAction">
                <param name="actionName">HelloWorld</param>
                <param name="namespace">/example</param>
            </result>
        </action>

        <action name="hello" class="com.example.struts.example.HelloAction">
            <result name="success">/WEB-INF/example/Hello.jsp</result>
        </action>

        <action name="upload" class="com.example.struts.example.UploadAction">
            <result name="success">/WEB-INF/example/Upload.jsp</result>
            <result name="input">/WEB-INF/example/Upload.jsp</result>
        </action>

        <action name="goHello">
            <result type="redirect">/hello.action?name=Redirected</result>
        </action>

    </package>
</struts>

常量说明:

  • struts.devMode=true:开发模式,便于排查(生产必须关闭);
  • struts.action.extension=do,action,:同时支持 .do、.action 与无后缀(末尾空项含义与实测见《Apache Struts 配置参数详解》);
  • struts.multipart.maxSize=10485760:上传上限 10MB(默认仅 2MB,第七节先按默认值实测超限,再调整到此值重新部署验证);
  • struts.enable.DynamicMethodInvocation=false:关闭动态方法调用(Login!method 语法),安全默认。

四、构建打包

4.1 首次打包报错与排障

cd /root/struts2-demo && mvn package

实测报错原文:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-war-plugin:2.2:war (default-war) on project struts2-demo: Execution default-war of goal org.apache.maven.plugins:maven-war-plugin:2.2:war failed: Unable to load the mojo 'war' in the plugin 'org.apache.maven.plugins:maven-war-plugin:2.2' due to an API incompatibility: org.codehaus.plexus.component.repository.exception.ComponentLookupException: Cannot access defaults field of Properties

⚠️ 排障结论:archetype 模板未固定 maven-war-plugin 版本,Maven 3.6.3 默认解析到古老的 2.2 版,其 API 与 JDK 17 不兼容(Cannot access defaults field of Properties)。修复方式是在 pom.xml 的 build/plugins 首位显式固定新版 maven-war-plugin。

在 pom.xml 的 <build><plugins> 中加入:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-war-plugin</artifactId>
    <version>3.4.0</version>
</plugin>

4.2 修复后重新打包

mvn package

实测回显(测试与打包结果):

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.207 sec
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.11 sec
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.763 sec
Tests run: 6, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
[INFO] Total time:  01:05 min

确认产物:

ls -lh target/*.war

实测回显:

-rw-r--r-- 1 root root 9.9M Oct 12 05:17 target/struts2-demo-1.0.0.war

五、部署到 Tomcat 9

5.1 方式一:复制 WAR 自动部署

cp /root/struts2-demo/target/struts2-demo-1.0.0.war /var/lib/tomcat9/webapps/struts2-demo.war
sleep 12 && ls /var/lib/tomcat9/webapps/

实测回显(war 已自动解压为同名目录):

ROOT
struts2-demo
struts2-demo.war

验证应用响应:

curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://localhost:8080/struts2-demo/

实测回显:

HTTP 200

war 包结构抽查:

unzip -l /var/lib/tomcat9/webapps/struts2-demo.war | head -12

实测回显:

Archive:  /var/lib/tomcat9/webapps/struts2-demo.war
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  2026-10-12 05:23   META-INF/
       81  2026-10-12 05:23   META-INF/MANIFEST.MF
        0  2026-10-12 05:17   WEB-INF/
        0  2026-10-12 05:17   WEB-INF/lib/
        0  2026-10-12 05:22   WEB-INF/example/
        0  2026-10-12 05:17   WEB-INF/classes/
        0  2026-10-12 05:17   WEB-INF/classes/com/
        0  2026-10-12 05:17   WEB-INF/classes/com/example/
        0  2026-10-12 05:17   WEB-INF/classes/com/example/struts/

5.2 方式二:Manager 文本接口部署

先配置 Manager 账号,在 /etc/tomcat9/tomcat-users.xml 的 <tomcat-users> 段内追加(admin / q1w2e3r4):

  <role rolename="manager-gui"/>
  <role rolename="manager-script"/>
  <user username="admin" password="q1w2e3r4" roles="manager-gui,manager-script"/>
</tomcat-users>

重启 Tomcat 并用文本接口查看应用列表:

systemctl restart tomcat9 && sleep 9
curl -s -u admin:q1w2e3r4 'http://localhost:8080/manager/text/list'

实测回显:

OK - Listed applications for virtual host [localhost]
/:running:0:ROOT
/host-manager:running:0:/usr/share/tomcat9-admin/host-manager
/struts2-demo:running:3:struts2-demo
/manager:running:0:/usr/share/tomcat9-admin/manager

卸载旧应用后再用 deploy 接口上传新 war(update=true):

curl -s -u admin:q1w2e3r4 'http://localhost:8080/manager/text/undeploy?path=/struts2-demo'

实测回显:

OK - Undeployed application at context path [/struts2-demo]
curl -s -u admin:q1w2e3r4 --upload-file /root/struts2-demo/target/struts2-demo-1.0.0.war 'http://localhost:8080/manager/text/deploy?path=/struts2-demo&update=true'

实测回显:

OK - Deployed application at context path [/struts2-demo]

部署后验证:

curl -s -o /dev/null -w 'app after manager deploy: HTTP %{http_code}\n' 'http://localhost:8080/struts2-demo/hello.action?name=M'

实测回显:

app after manager deploy: HTTP 200

⚠️ 安全提示:manager 账号密码务必避免弱口令,生产环境还应通过网络策略或反向代理限制 /manager 的访问来源。

六、页面功能验证

6.1 首页与跳转链

应用首页 index.html 通过 meta refresh 进入,index action 配置了 redirectAction 到 /example 命名空间的 HelloWorld。实测任意 .action 后缀的未匹配请求(如 foo.action)会被 default-action-ref index 兜底重定向:

curl -sI 'http://localhost:8080/struts2-demo/foo.action' | grep -i '^HTTP\|^Location'

实测回显:

HTTP/1.1 302
Location: /struts2-demo/example/HelloWorld.action

HelloWorld 页面渲染:

curl -s 'http://localhost:8080/struts2-demo/example/HelloWorld.action' | grep -E '<h2>|<title>'

实测回显:

    <title>Struts is up and running...</title>
<h2>Struts is up and running...</h2>

6.2 自定义 hello(参数注入与标签渲染)

带 name 参数访问:

curl -s 'http://localhost:8080/struts2-demo/hello.action?name=Struts' | sed -n '1,20p'

实测回显:


<html>
<head>
    <title>Hello Struts</title>
</head>

<body>
<h2>Hello, Struts! This page is rendered by Struts tags.</h2>

<h3>Environment features</h3>
<ul>

        <li>Apache Struts 6.12.0</li>

        <li>Apache Tomcat 9.0.58</li>

        <li>OpenJDK 17.0.20.1</li>

</ul>

不带参数时 execute() 中兜底为 World:

curl -s 'http://localhost:8080/struts2-demo/hello.action' | grep -E '<h2>'

实测回显:

<h2>Hello, World! This page is rendered by Struts tags.</h2>

6.3 goHello 重定向

curl -sI 'http://localhost:8080/struts2-demo/goHello.action' | grep -i '^HTTP\|^Location'

实测回显:

HTTP/1.1 302
Location: /struts2-demo/hello.action?name=Redirected

6.4 登录与校验流程

访问登录表单(Login_input.action 经通配符映射到 input() 方法,s:form 标签自动生成表单 HTML):

curl -s 'http://localhost:8080/struts2-demo/example/Login_input.action' | grep -E '<form|<input|label' | head -8

实测回显:

<form id="Login" name="Login" action="/struts2-demo/example/Login.action;jsessionid=09C9821A0C08019E1C349EFF0B298FFE" method="post">
    <td class="tdLabel"><label for="Login_username" class="label">User Name:</label></td>
><input type="text" name="username" value="" id="Login_username"/></td>
    <td class="tdLabel"><label for="Login_password" class="label">Password:</label></td>
><input type="password" name="password" id="Login_password"/></td>
<td colspan="2"><div class="formButton"><input type="submit" value="Submit" id="Login_0"/>

空表单提交触发 requiredstring 校验,回到 input 页并显示错误:

curl -s -X POST 'http://localhost:8080/struts2-demo/example/Login_execute.action' | grep -E 'errorField|errorMessage|<form' | head -6

实测回显:

<form id="Login" name="Login" action="/struts2-demo/example/Login.action;jsessionid=B32202CFBDE532DA2D25BA410C134C7D" method="post">
    <td class="tdErrorMessage" colspan="2"><span class="errorMessage">User Name is required.</span></td>
    <td class="tdErrorMessage" colspan="2"><span class="errorMessage">Password is required.</span></td>

提交合法账号(admin / q1w2e3r4)后校验通过,302 跳转 Menu:

curl -s -o /dev/null -w 'HTTP %{http_code} redirect->%{redirect_url}\n' -X POST -d 'username=admin&password=q1w2e3r4' 'http://localhost:8080/struts2-demo/example/Login_execute.action'

实测回显:

HTTP 302 redirect->http://localhost:8080/struts2-demo/example/Menu.action

Menu 页面(Menu.jsp 内嵌 Missing.jsp,i18n 文案渲染):

curl -s 'http://localhost:8080/struts2-demo/example/Menu.action' | head -20

实测回显:

<html>
<head><title>Missing Feature</title></head>

<body>
<p>
    This feature is under construction. Please try again in the next iteration.
</p>
</body>
</html>

6.5 国际化切换

通过 request_locale=es 请求参数切换语言(I18nInterceptor 默认支持):

curl -s 'http://localhost:8080/struts2-demo/example/HelloWorld.action?request_locale=es' | grep -E '<h2>|<title>'

实测回显:

    <title>¡Struts está bien!...</title>
<h2>&iexcl;Struts est&aacute; bien!...</h2>

切回默认英文:

curl -s 'http://localhost:8080/struts2-demo/example/HelloWorld.action' | grep -E '<h2>'

实测回显:

<h2>Struts is up and running...</h2>

6.6 动态方法调用(DMI)默认关闭

struts.enable.DynamicMethodInvocation=false 时,Login!method 感叹号语法直接 404:

curl -s -o /dev/null -w 'HTTP %{http_code}\n' 'http://localhost:8080/struts2-demo/example/Login!execute.action'

实测回显:

HTTP 404

七、文件上传实测

7.1 默认上限(2MB)下的行为

先按 struts.xml 未设置 struts.multipart.maxSize 的默认情况验证。构造 1MB 文件上传,成功保存到 /tmp:

dd if=/dev/zero of=/tmp/small.bin bs=1M count=1
curl -s -F 'upload=@/tmp/small.bin;type=application/octet-stream' 'http://localhost:8080/struts2-demo/upload.action' | grep -E 'Saved'

实测回显:

<p>Saved small.bin (1048576 bytes, content-type=application/octet-stream) to /tmp/small.bin</p>

构造 3MB 文件上传,触发默认上限 2097152(2MB),action 捕获错误并回显:

dd if=/dev/zero of=/tmp/big.bin bs=1M count=3
curl -s -F 'upload=@/tmp/big.bin;type=application/octet-stream' 'http://localhost:8080/struts2-demo/upload.action' | grep -A2 'errorMessage'

实测回显:

        <ul class="errorMessage">
            <li><span>Request exceeded allowed size limit! Max size allowed is: 2,097,152 but request was: 3,145,929!</span></li>       </ul>

7.2 提高上限到 10MB

在 struts.xml 加入 <constant name="struts.multipart.maxSize" value="10485760"/> 后重新 mvn package 并按第五节重新部署,3MB 上传成功(此处同时已启用 .do 后缀,用 upload.do 访问验证 extension 常量生效):

curl -s -F 'upload=@/tmp/big.bin;type=application/octet-stream' 'http://localhost:8080/struts2-demo/upload.do' | grep -E 'Saved'

实测回显:

<p>Saved big.bin (3145728 bytes, content-type=application/octet-stream) to /tmp/big.bin</p>

7.3 临时目录行为

未设置 struts.multipart.saveDir 时,日志会提示回退到 Web 应用临时目录:

2026-10-12 05:21:37,936 INFO  [http-nio-8080-exec-9] dispatcher.Dispatcher (Dispatcher.java:1027) - Unable to find 'struts.multipart.saveDir' property setting. Defaulting to javax.servlet.context.tempdir

该提示为 INFO 级,不影响功能;如需固定上传临时目录,可在 struts.xml 中显式设置 struts.multipart.saveDir。

八、devMode 与日志排查

8.1 devMode 生效确认

struts.xml 设置 struts.devMode=true 后,服务日志会出现 devMode 告警(journalctl 查看):

journalctl -u tomcat9 --no-pager | grep -iE 'struts|devMode' | head

实测回显:

Oct 12 05:18:05 8a0740394d5a tomcat9[6295]: Deploying web application archive [/var/lib/tomcat9/webapps/struts2-demo.war]
Oct 12 05:18:07 8a0740394d5a tomcat9[6295]: 2026-10-12 05:18:07,504 WARN  [Catalina-utility-1] ognl.SecurityMemberAccess (SecurityMemberAccess.java:544) - Working in devMode, using devMode excluded classes and packages!

8.2 部署日志

Tomcat 自身日志(按日滚动)记录 war 部署过程:

12-Oct-2026 05:18:05.350 INFO [Catalina-utility-1] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive [/var/lib/tomcat9/webapps/struts2-demo.war]
12-Oct-2026 05:18:07.752 INFO [Catalina-utility-1] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [/var/lib/tomcat9/webapps/struts2-demo.war] has finished in [2,402] ms

8.3 日志文件位置

日志路径 / 命令内容
Tomcat 运行日志/var/log/tomcat9/catalina.2026-10-12.log启动、部署、停机(按日滚动)
应用级日志/var/log/tomcat9/localhost.2026-10-12.logWeb 应用异常
访问日志/var/log/tomcat9/localhost_access_log.2026-10-12.txtHTTP 访问记录
服务单元日志journalctl -u tomcat9应用 stdout 与 log4j2 输出(Ubuntu 的 tomcat9 无 catalina.out)

九、内嵌 Jetty 运行(开发期免部署)

pom 已内置 jetty-maven-plugin(contextPath /struts2-demo),开发期无需部署 war,直接内嵌运行(指定 8081 端口避免与 Tomcat 冲突):

cd /root/struts2-demo && nohup mvn jetty:run -Djetty.http.port=8081 > /tmp/jetty.log 2>&1 &

首次运行需下载 Jetty 插件依赖,实测约 151 秒后启动完成,/tmp/jetty.log 中出现:

[INFO] Started ServerConnector@64d53f0d{HTTP/1.1, (http/1.1)}{0.0.0.0:8081}
[INFO] Started @151097ms
[INFO] Started Jetty Server

验证(注意 contextPath):

curl -s 'http://localhost:8081/struts2-demo/hello.action?name=Jetty' | grep '<h2>'

实测回显:

<h2>Hello, Jetty! This page is rendered by Struts tags.</h2>

十、常见问题

10.1 打包报 Cannot access defaults field of Properties

maven-war-plugin 2.2 与 JDK 17 不兼容,按 4.1 节在 pom 中固定 maven-war-plugin 3.4.0 后重新打包即可。判断依据:报错目标为 maven-war-plugin:2.2:war。

10.2 上传报 Request exceeded allowed size limit

默认上传上限为 2097152 字节(2MB),来自 struts2-core 内置默认值。按 7.2 节在 struts.xml 设置 struts.multipart.maxSize(如 10485760)后重新打包部署生效。

10.3 日志出现 Unable to find 'struts.multipart.saveDir'

INFO 级提示,表示未配置上传临时目录、已回退到 Web 应用临时目录,功能不受影响。需要固定目录时在 struts.xml 设置 struts.multipart.saveDir。

10.4 Action!method 形式的 URL 返回 404

Struts 6.x 默认关闭动态方法调用(struts.enable.DynamicMethodInvocation=false),感叹号语法被拒绝,属安全默认行为。如需按方法分发,使用通配符(如本示例的 Login_*)或 DMR 方式,不建议打开 DMI。

十一、清理与卸载

通过 Manager 卸载应用并删除产物(工程目录可保留供重复实验):

curl -s -u admin:q1w2e3r4 'http://localhost:8080/manager/text/undeploy?path=/struts2-demo'
rm -f /var/lib/tomcat9/webapps/struts2-demo.war
rm -rf /var/lib/tomcat9/webapps/struts2-demo
rm -rf /root/struts2-demo/target

停止内嵌 Jetty:

pkill -f 'jetty:run'

十二、参考链接