From 0bbfdd1d7a895e40866df6596e6b0a3b470f7608 Mon Sep 17 00:00:00 2001 From: likingcode Date: Mon, 9 Mar 2026 23:57:50 +0800 Subject: [PATCH] feat(interactive): enhance struts learning pages with task cards and quick experiments --- .../com/example/struts2/CalculatorAction.java | 77 ++++++++++++ .../java/com/example/struts2/HelloAction.java | 15 +++ .../struts2/InterceptorDemoAction.java | 36 ++++++ .../java/com/example/struts2/LearnAction.java | 60 ++++++++++ .../com/example/struts2/UserFormAction.java | 106 +++++++++++++++++ .../interceptor/LoggingInterceptor.java | 38 ++++++ .../interceptor/MonitorInterceptor.java | 48 ++++++++ .../interceptor/RateLimitInterceptor.java | 86 ++++++++++++++ .../interceptor/TimingInterceptor.java | 51 ++++++++ .../interceptor/ValidationInterceptor.java | 44 +++++++ src/main/resources/struts.xml | 103 ++++++++++++++++ src/main/webapp/calculator.jsp | 85 +++++++++++++ src/main/webapp/interceptor-demo.jsp | 80 +++++++++++++ src/main/webapp/learn.jsp | 112 ++++++++++++++++++ 14 files changed, 941 insertions(+) create mode 100644 src/main/java/com/example/struts2/CalculatorAction.java create mode 100644 src/main/java/com/example/struts2/HelloAction.java create mode 100644 src/main/java/com/example/struts2/InterceptorDemoAction.java create mode 100644 src/main/java/com/example/struts2/LearnAction.java create mode 100644 src/main/java/com/example/struts2/UserFormAction.java create mode 100644 src/main/java/com/example/struts2/interceptor/LoggingInterceptor.java create mode 100644 src/main/java/com/example/struts2/interceptor/MonitorInterceptor.java create mode 100644 src/main/java/com/example/struts2/interceptor/RateLimitInterceptor.java create mode 100644 src/main/java/com/example/struts2/interceptor/TimingInterceptor.java create mode 100644 src/main/java/com/example/struts2/interceptor/ValidationInterceptor.java create mode 100644 src/main/resources/struts.xml create mode 100644 src/main/webapp/calculator.jsp create mode 100644 src/main/webapp/interceptor-demo.jsp create mode 100644 src/main/webapp/learn.jsp diff --git a/src/main/java/com/example/struts2/CalculatorAction.java b/src/main/java/com/example/struts2/CalculatorAction.java new file mode 100644 index 0000000..b5fc03b --- /dev/null +++ b/src/main/java/com/example/struts2/CalculatorAction.java @@ -0,0 +1,77 @@ +package com.example.struts2; + +import com.opensymphony.xwork2.ActionSupport; + +/** + * 计算器 Action - 表单处理示例 + * + * 学习点: + * - Action 接收表单参数(通过 setter) + * - 数据验证(validate 方法) + * - 结果类型 + */ +public class CalculatorAction extends ActionSupport { + + private Double num1; + private Double num2; + private String operator; + private Double result; + + // 默认执行方法 + public String execute() { + return SUCCESS; + } + + // 计算方法 + public String calculate() { + if (num1 == null || num2 == null || operator == null) { + return INPUT; + } + + switch (operator) { + case "+": + result = num1 + num2; + break; + case "-": + result = num1 - num2; + break; + case "*": + result = num1 * num2; + break; + case "/": + if (num2 == 0) { + addFieldError("num2", "除数不能为0"); + return INPUT; + } + result = num1 / num2; + break; + default: + return INPUT; + } + return SUCCESS; + } + + // 验证方法 + @Override + public void validate() { + if (num1 == null) { + addFieldError("num1", "请输入第一个数字"); + } + if (num2 == null) { + addFieldError("num2", "请输入第二个数字"); + } + } + + // Getter and Setter(Struts2 通过这些接收参数) + public Double getNum1() { return num1; } + public void setNum1(Double num1) { this.num1 = num1; } + + public Double getNum2() { return num2; } + public void setNum2(Double num2) { this.num2 = num2; } + + public String getOperator() { return operator; } + public void setOperator(String operator) { this.operator = operator; } + + public Double getResult() { return result; } + public void setResult(Double result) { this.result = result; } +} \ No newline at end of file diff --git a/src/main/java/com/example/struts2/HelloAction.java b/src/main/java/com/example/struts2/HelloAction.java new file mode 100644 index 0000000..ed1e674 --- /dev/null +++ b/src/main/java/com/example/struts2/HelloAction.java @@ -0,0 +1,15 @@ +package com.example.struts2; + +import com.opensymphony.xwork2.ActionSupport; + +public class HelloAction extends ActionSupport { + private String message = "Hello from Struts2 Scaffold!"; + + public String execute() { + return SUCCESS; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/com/example/struts2/InterceptorDemoAction.java b/src/main/java/com/example/struts2/InterceptorDemoAction.java new file mode 100644 index 0000000..4f6038b --- /dev/null +++ b/src/main/java/com/example/struts2/InterceptorDemoAction.java @@ -0,0 +1,36 @@ +package com.example.struts2; + +import com.opensymphony.xwork2.ActionSupport; +import com.example.struts2.interceptor.MonitorInterceptor; +import org.apache.struts2.interceptor.RequestAware; +import java.util.Map; + +/** + * 拦截器演示 Action + * + * 学习点: + * - 拦截器与 Action 的协作 + * - RequestAware 接口获取 request + */ +public class InterceptorDemoAction extends ActionSupport implements RequestAware { + + private Map request; + private Map interceptorStats; + private Long executionTime; + + public String execute() { + interceptorStats = MonitorInterceptor.getStats(); + if (request != null) { + executionTime = (Long) request.get("executionTimeNs"); + } + return SUCCESS; + } + + @Override + public void setRequest(Map request) { + this.request = request; + } + + public Map getInterceptorStats() { return interceptorStats; } + public Long getExecutionTime() { return executionTime; } +} \ No newline at end of file diff --git a/src/main/java/com/example/struts2/LearnAction.java b/src/main/java/com/example/struts2/LearnAction.java new file mode 100644 index 0000000..b426c51 --- /dev/null +++ b/src/main/java/com/example/struts2/LearnAction.java @@ -0,0 +1,60 @@ +package com.example.struts2; + +import com.opensymphony.xwork2.ActionSupport; +import javax.servlet.http.HttpServletRequest; +import org.apache.struts2.interceptor.ServletRequestAware; +import java.util.HashMap; +import java.util.Map; + +/** + * 学习示例 Action + * + * 学习点: + * - 获取原始 HttpServletRequest + * - 多种结果类型 + * - Action 配置 + */ +public class LearnAction extends ActionSupport implements ServletRequestAware { + + private HttpServletRequest request; + private Map data; + + // 默认方法 - 显示学习菜单 + public String execute() { + data = new HashMap<>(); + data.put("message", "欢迎学习 Struts2!"); + data.put("features", new String[]{ + "MVC 架构模式", + "拦截器机制", + "OGNL 表达式语言", + "标签库", + "类型转换", + "输入验证" + }); + data.put("actions", new String[]{ + "hello - 基础示例", + "calculator - 表单处理", + "user - CRUD 操作", + "learn/info - 本页面" + }); + return SUCCESS; + } + + // 显示请求信息 + public String info() { + data = new HashMap<>(); + data.put("method", request.getMethod()); + data.put("contextPath", request.getContextPath()); + data.put("remoteAddr", request.getRemoteAddr()); + data.put("header_UserAgent", request.getHeader("User-Agent")); + return SUCCESS; + } + + @Override + public void setServletRequest(HttpServletRequest request) { + this.request = request; + } + + public Map getData() { return data; } + public void setData(Map data) { this.data = data; } +} \ No newline at end of file diff --git a/src/main/java/com/example/struts2/UserFormAction.java b/src/main/java/com/example/struts2/UserFormAction.java new file mode 100644 index 0000000..c17361c --- /dev/null +++ b/src/main/java/com/example/struts2/UserFormAction.java @@ -0,0 +1,106 @@ +package com.example.struts2; + +import com.opensymphony.xwork2.ActionSupport; +import java.util.ArrayList; +import java.util.List; + +/** + * 用户管理 Action - CRUD 示例 + * + * 学习点: + * - Session 范围数据 + * - 多个处理方法 + * - 动态方法调用 + */ +public class UserFormAction extends ActionSupport { + + private static List users = new ArrayList<>(); + private static Long idCounter = 1L; + + private User user = new User(); + private Long id; + private List userList; + + static { + users.add(new User(idCounter++, "张三", "zhangsan@example.com", 25)); + users.add(new User(idCounter++, "李四", "lisi@example.com", 30)); + users.add(new User(idCounter++, "王五", "wangwu@example.com", 28)); + } + + // 列表 + public String list() { + userList = new ArrayList<>(users); + return SUCCESS; + } + + // 添加 + public String add() { + user.setId(idCounter++); + users.add(user); + return SUCCESS; + } + + // 编辑页面 + public String edit() { + for (User u : users) { + if (u.getId().equals(id)) { + user = u; + break; + } + } + return SUCCESS; + } + + // 更新 + public String update() { + for (int i = 0; i < users.size(); i++) { + if (users.get(i).getId().equals(user.getId())) { + users.set(i, user); + break; + } + } + return SUCCESS; + } + + // 删除 + public String delete() { + users.removeIf(u -> u.getId().equals(id)); + return SUCCESS; + } + + // Getters and Setters + public User getUser() { return user; } + public void setUser(User user) { this.user = user; } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public List getUserList() { return userList; } + public void setUserList(List userList) { this.userList = userList; } + + // 内部类 - 用户实体 + public static class User { + private Long id; + private String name; + private String email; + private Integer age; + + public User() {} + + public User(Long id, String name, String email, Integer age) { + this.id = id; + this.name = name; + this.email = email; + this.age = age; + } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public Integer getAge() { return age; } + public void setAge(Integer age) { this.age = age; } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/struts2/interceptor/LoggingInterceptor.java b/src/main/java/com/example/struts2/interceptor/LoggingInterceptor.java new file mode 100644 index 0000000..48bcc12 --- /dev/null +++ b/src/main/java/com/example/struts2/interceptor/LoggingInterceptor.java @@ -0,0 +1,38 @@ +package com.example.struts2.interceptor; + +import com.opensymphony.xwork2.ActionInvocation; +import com.opensymphony.xwork2.interceptor.AbstractInterceptor; +import java.util.Map; + +/** + * 日志拦截器 - 演示自定义拦截器 + * + * 学习点: + * - AbstractInterceptor 基类 + * - intercept() 方法 + * - ActionInvocation 对象 + * - 拦截器链执行顺序 + */ +public class LoggingInterceptor extends AbstractInterceptor { + + @Override + public String intercept(ActionInvocation invocation) throws Exception { + String actionName = invocation.getProxy().getActionName(); + String method = invocation.getProxy().getMethod(); + + System.out.println("[LoggingInterceptor] 进入 Action: " + actionName + + ", Method: " + method); + + long startTime = System.currentTimeMillis(); + + // 执行下一个拦截器或 Action + String result = invocation.invoke(); + + long duration = System.currentTimeMillis() - startTime; + + System.out.println("[LoggingInterceptor] 退出 Action: " + actionName + + ", Result: " + result + ", 耗时: " + duration + "ms"); + + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/struts2/interceptor/MonitorInterceptor.java b/src/main/java/com/example/struts2/interceptor/MonitorInterceptor.java new file mode 100644 index 0000000..e9506fd --- /dev/null +++ b/src/main/java/com/example/struts2/interceptor/MonitorInterceptor.java @@ -0,0 +1,48 @@ +package com.example.struts2.interceptor; + +import com.opensymphony.xwork2.ActionInvocation; +import com.opensymphony.xwork2.interceptor.Interceptor; +import com.opensymphony.xwork2.ActionContext; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 拦截器状态监控 - 演示拦截器生命周期 + */ +public class MonitorInterceptor implements Interceptor { + + private static final Map actionCallCounts = new ConcurrentHashMap<>(); + private static long interceptorInitTime; + + @Override + public void init() { + interceptorInitTime = System.currentTimeMillis(); + System.out.println("[MonitorInterceptor] 初始化 - " + new java.util.Date()); + } + + @Override + public void destroy() { + System.out.println("[MonitorInterceptor] 销毁 - " + new java.util.Date()); + System.out.println("[MonitorInterceptor] 最终统计: " + actionCallCounts); + } + + @Override + public String intercept(ActionInvocation invocation) throws Exception { + String actionName = invocation.getProxy().getActionName(); + actionCallCounts.merge(actionName, 1L, Long::sum); + + // 通过 invocation 获取 context + ActionContext context = invocation.getInvocationContext(); + Map request = (Map) context.get("request"); + if (request != null) { + request.put("interceptorInitTime", interceptorInitTime); + request.put("actionCallCounts", new ConcurrentHashMap<>(actionCallCounts)); + } + + return invocation.invoke(); + } + + public static Map getStats() { + return new ConcurrentHashMap<>(actionCallCounts); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/struts2/interceptor/RateLimitInterceptor.java b/src/main/java/com/example/struts2/interceptor/RateLimitInterceptor.java new file mode 100644 index 0000000..aa156dc --- /dev/null +++ b/src/main/java/com/example/struts2/interceptor/RateLimitInterceptor.java @@ -0,0 +1,86 @@ +package com.example.struts2.interceptor; + +import com.opensymphony.xwork2.ActionInvocation; +import com.opensymphony.xwork2.interceptor.AbstractInterceptor; +import com.opensymphony.xwork2.ActionContext; +import javax.servlet.http.HttpServletRequest; +import org.apache.struts2.StrutsStatics; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 限流拦截器 - 演示拦截器实现横切关注点 + * + * 学习点: + * - 拦截器实现非功能性需求 + * - IP 限流算法 + * - 返回自定义结果 + */ +public class RateLimitInterceptor extends AbstractInterceptor { + + // IP 请求计数器 + private final Map ipRequestCounts = new ConcurrentHashMap<>(); + private final Map ipLastAccess = new ConcurrentHashMap<>(); + + // 每分钟最大请求数 + private long maxRequestsPerMinute = 60; + + @Override + public String intercept(ActionInvocation invocation) throws Exception { + // 获取客户端 IP + String clientIp = getClientIp(invocation); + + long currentTime = System.currentTimeMillis(); + Long lastAccess = ipLastAccess.get(clientIp); + + // 每分钟重置计数器 + if (lastAccess == null || currentTime - lastAccess > 60000) { + ipRequestCounts.put(clientIp, new AtomicLong(0)); + ipLastAccess.put(clientIp, currentTime); + } + + AtomicLong count = ipRequestCounts.get(clientIp); + long currentCount = count.incrementAndGet(); + + if (currentCount > maxRequestsPerMinute) { + System.out.println("[RateLimitInterceptor] IP: " + clientIp + + " 超过限流阈值: " + currentCount); + return "rateLimitExceeded"; + } + + System.out.println("[RateLimitInterceptor] IP: " + clientIp + + " 请求计数: " + currentCount + "/" + maxRequestsPerMinute); + + return invocation.invoke(); + } + + /** + * 获取客户端真实 IP + */ + private String getClientIp(ActionInvocation invocation) { + ActionContext context = invocation.getInvocationContext(); + HttpServletRequest request = (HttpServletRequest) + context.get(StrutsStatics.HTTP_REQUEST); + + String ip = request.getHeader("X-Forwarded-For"); + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Real-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + + // 多个代理时取第一个 + if (ip != null && ip.contains(",")) { + ip = ip.split(",")[0].trim(); + } + + return ip; + } + + // Getter/Setter for configuration + public void setMaxRequestsPerMinute(long max) { + this.maxRequestsPerMinute = max; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/struts2/interceptor/TimingInterceptor.java b/src/main/java/com/example/struts2/interceptor/TimingInterceptor.java new file mode 100644 index 0000000..fcb010e --- /dev/null +++ b/src/main/java/com/example/struts2/interceptor/TimingInterceptor.java @@ -0,0 +1,51 @@ +package com.example.struts2.interceptor; + +import com.opensymphony.xwork2.ActionInvocation; +import com.opensymphony.xwork2.interceptor.AbstractInterceptor; +import com.opensymphony.xwork2.ActionContext; +import java.util.Map; + +/** + * 计时拦截器 - 演示拦截器统计功能 + * + * 学习点: + * - 拦截器状态保持 + * - ActionContext 获取上下文 + * - 多个拦截器协作 + */ +public class TimingInterceptor extends AbstractInterceptor { + + // 统计数据 + private long totalRequests = 0; + private long totalTime = 0; + + @Override + public String intercept(ActionInvocation invocation) throws Exception { + totalRequests++; + + String actionName = invocation.getProxy().getActionName(); + long startTime = System.nanoTime(); + + // 执行 Action + String result = invocation.invoke(); + + long duration = System.nanoTime() - startTime; + totalTime += duration; + + // 存储到 request 作用域 + Map request = (Map) + ActionContext.getContext().get("request"); + if (request != null) { + request.put("executionTimeNs", duration); + request.put("totalRequests", totalRequests); + } + + System.out.println("[TimingInterceptor] " + actionName + + " 执行时间: " + (duration / 1_000_000.0) + " ms"); + + return result; + } + + public long getTotalRequests() { return totalRequests; } + public long getTotalTime() { return totalTime; } +} \ No newline at end of file diff --git a/src/main/java/com/example/struts2/interceptor/ValidationInterceptor.java b/src/main/java/com/example/struts2/interceptor/ValidationInterceptor.java new file mode 100644 index 0000000..5b365c4 --- /dev/null +++ b/src/main/java/com/example/struts2/interceptor/ValidationInterceptor.java @@ -0,0 +1,44 @@ +package com.example.struts2.interceptor; + +import com.opensymphony.xwork2.ActionInvocation; +import com.opensymphony.xwork2.interceptor.AbstractInterceptor; +import com.opensymphony.xwork2.ActionContext; +import org.apache.struts2.dispatcher.HttpParameters; +import java.util.Map; + +/** + * 参数验证拦截器 - 演示拦截器预处理 + */ +public class ValidationInterceptor extends AbstractInterceptor { + + @Override + public String intercept(ActionInvocation invocation) throws Exception { + System.out.println("[ValidationInterceptor] 开始参数验证"); + + // 获取参数 (Struts2 6.x 使用 HttpParameters) + HttpParameters params = invocation.getInvocationContext().getParameters(); + + for (String paramName : params.keySet()) { + String[] values = params.get(paramName).getMultipleValues(); + if (values != null) { + for (String value : values) { + if (containsXss(value)) { + System.out.println("[ValidationInterceptor] 检测到可疑参数: " + paramName); + return "invalidInput"; + } + } + } + } + + System.out.println("[ValidationInterceptor] 参数验证通过"); + return invocation.invoke(); + } + + private boolean containsXss(String value) { + if (value == null) return false; + String lower = value.toLowerCase(); + return lower.contains(" + + + + + + + + + + + + + + + + + + + + + + + + + + + + 100 + + + + + + + + + + + + + + + + /error-rate-limit.jsp + /error-invalid-input.jsp + + + + + /index.jsp + + + + /hello.jsp + + + + /learn.jsp + + + + /interceptor-demo.jsp + + + + + /interceptor-demo.jsp + + + + /calculator.jsp + + + + /calculator.jsp + /calculator.jsp + + + + /user-list.jsp + + + + user + + + + /user-form.jsp + + + + user + + + + user + + + + \ No newline at end of file diff --git a/src/main/webapp/calculator.jsp b/src/main/webapp/calculator.jsp new file mode 100644 index 0000000..de5dab5 --- /dev/null +++ b/src/main/webapp/calculator.jsp @@ -0,0 +1,85 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="s" uri="/struts-tags" %> + + + + 计算器 - Struts2 表单示例 + + + +

🔢 计算器 - Struts2 表单示例

+ + +
+ 计算结果: + + + = +
+
+ +
+ 快速实验: + + + +
+ + +
+ + + +
+ +
+ + +
+ +
+ + + +
+ +
+ +
+
+ +
+ 学习点: +
    +
  • name 属性对应 Action 的 setter 方法
  • +
  • <s:fielderror> 显示验证错误
  • +
  • validate() 方法进行数据验证
  • +
+
+ +

← 返回学习中心

+ + + + diff --git a/src/main/webapp/interceptor-demo.jsp b/src/main/webapp/interceptor-demo.jsp new file mode 100644 index 0000000..8d8f939 --- /dev/null +++ b/src/main/webapp/interceptor-demo.jsp @@ -0,0 +1,80 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="s" uri="/struts-tags" %> + + + + 拦截器演示 - Struts2 + + + +

🛡️ Struts2 拦截器演示

+ +
+ 实验观察点: +
    +
  • 连续刷新页面,观察 Action 调用次数如何累积
  • +
  • 切换到“API 限流栈”,理解为什么拦截器顺序会影响结果
  • +
  • 注意执行时间显示,这就是拦截器注入到 request 的数据
  • +
+
+ +
+

📊 执行统计

+

本次请求执行时间: ms

+ +

Action 调用统计:

+ + + + + + + + +
Action调用次数
+
+ +
+

🔗 拦截器链演示

+

当前使用: customStack (日志 + 计时 + 监控 + 默认栈)

+ 使用 API 限流栈 +

API 栈包含: 日志 + 限流 + 验证 + 计时 + 默认栈

+
+ +
+

📚 拦截器执行顺序

+
+请求 → LoggingInterceptor → TimingInterceptor → MonitorInterceptor → Action
+响应 ← LoggingInterceptor ← TimingInterceptor ← MonitorInterceptor ← Result
+        
+

注意: 拦截器像洋葱一样,先进入的后退出

+
+ +
+

💡 拦截器核心概念

+
    +
  • Interceptor 接口: init() → intercept() → destroy()
  • +
  • intercept() 方法: 调用 invocation.invoke() 继续链
  • +
  • 拦截器栈: 多个拦截器按顺序组成链
  • +
  • 责任链模式: 每个拦截器决定是否继续
  • +
+
+ +

← 返回学习中心

+ + \ No newline at end of file diff --git a/src/main/webapp/learn.jsp b/src/main/webapp/learn.jsp new file mode 100644 index 0000000..5c3746a --- /dev/null +++ b/src/main/webapp/learn.jsp @@ -0,0 +1,112 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="s" uri="/struts-tags" %> + + + + Struts2 学习中心 + + + +

🎓 Struts2 学习中心

+ +
+ 🧪 学习任务卡 +
    +
  • 先点“拦截器演示”,观察执行统计和链路顺序
  • +
  • 再点“计算器”,故意输入非法值体验 Struts2 验证
  • +
  • 最后点“用户管理”,理解 Action + JSP + 表单提交流程
  • +
+ +
+ +
+

🛡️ 拦截器 (核心特性)

+

拦截器是 Struts2 最强大的特性之一,基于责任链模式实现 AOP。

+ + +
+ +
+

📝 基础示例

+ +
+ +

📚 学习路径

+ +
+

1. MVC 架构

+
    +
  • Model: Action 类 + JavaBean
  • +
  • View: JSP + Struts 标签库
  • +
  • Controller: StrutsPrepareAndExecuteFilter
  • +
+
+ +
+

2. 拦截器机制

+
    +
  • Interceptor 接口: init() → intercept() → destroy()
  • +
  • interceptor-stack: 组合多个拦截器
  • +
  • 执行顺序: 责任链模式(先入后出)
  • +
+
+ +
+

3. OGNL 表达式

+
    +
  • <s:property value="name"/> - 输出属性
  • +
  • <s:iterator value="list"> - 遍历集合
  • +
  • #request.key - 访问 request
  • +
+
+ +
+

4. 项目结构

+
+├── src/main/java/com/example/struts2/
+│   ├── action/          # Action 类
+│   └── interceptor/     # 自定义拦截器
+├── src/main/resources/
+│   └── struts.xml       # 核心配置
+└── src/main/webapp/
+    ├── WEB-INF/web.xml  # Servlet 配置
+    └── *.jsp            # 视图页面
+        
+
+ +

← 返回首页

+ + \ No newline at end of file