feat(structs): add OGNL binding lab and deepen interceptor learning page

This commit is contained in:
likingcode
2026-03-10 06:57:33 +08:00
parent 67f800c3cb
commit da75afbc93
6 changed files with 192 additions and 1 deletions

View File

@@ -0,0 +1,71 @@
package com.example.struts2;
import com.opensymphony.xwork2.ActionSupport;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class OgnlLabAction extends ActionSupport {
private String keyword;
private Integer minAge;
private DemoUser user = new DemoUser();
private List<String> tags = new ArrayList<>();
private List<DemoUser> users;
private List<DemoUser> filteredUsers;
public String execute() {
loadUsers();
filteredUsers = users;
return SUCCESS;
}
public String bind() {
loadUsers();
filteredUsers = users.stream()
.filter(u -> keyword == null || keyword.isBlank() || u.getName().contains(keyword) || u.getEmail().contains(keyword))
.filter(u -> minAge == null || u.getAge() >= minAge)
.collect(Collectors.toList());
return SUCCESS;
}
private void loadUsers() {
users = Arrays.asList(
new DemoUser("张三", "zhangsan@example.com", 22),
new DemoUser("李四", "lisi@example.com", 28),
new DemoUser("王五", "wangwu@example.com", 34),
new DemoUser("赵六", "zhaoliu@example.com", 19)
);
}
public static class DemoUser {
private String name;
private String email;
private Integer age;
public DemoUser() {}
public DemoUser(String name, String email, Integer age) {
this.name = name;
this.email = email;
this.age = age;
}
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; }
}
public String getKeyword() { return keyword; }
public void setKeyword(String keyword) { this.keyword = keyword; }
public Integer getMinAge() { return minAge; }
public void setMinAge(Integer minAge) { this.minAge = minAge; }
public DemoUser getUser() { return user; }
public void setUser(DemoUser user) { this.user = user; }
public List<String> getTags() { return tags; }
public void setTags(List<String> tags) { this.tags = tags; }
public List<DemoUser> getUsers() { return users; }
public List<DemoUser> getFilteredUsers() { return filteredUsers; }
}

View File

@@ -64,6 +64,14 @@
<action name="interceptor" class="com.example.struts2.InterceptorDemoAction">
<result>/interceptor-demo.jsp</result>
</action>
<action name="ognl" class="com.example.struts2.OgnlLabAction">
<result>/ognl-lab.jsp</result>
</action>
<action name="ognl_bind" class="com.example.struts2.OgnlLabAction" method="bind">
<result>/ognl-lab.jsp</result>
</action>
<action name="interceptor_api" class="com.example.struts2.InterceptorDemoAction">
<interceptor-ref name="apiStack"/>

View File

@@ -61,6 +61,11 @@
<p>从列表页到表单页,再到新增/编辑/删除,形成完整的 Struts2 业务流学习闭环。</p>
<a href="user" class="btn green">进入 CRUD</a>
</div>
<div class="card">
<h3>🧪 OGNL / 参数绑定实验</h3>
<p>可视化体验普通字段、嵌套对象、多选列表是如何被 Struts2 自动绑定到 Action 的。</p>
<a href="ognl" class="btn purple">进入实验室</a>
</div>
</div>
<div class="timeline">

View File

@@ -53,10 +53,29 @@
<div class="card">
<h3>🔗 拦截器链演示</h3>
<p>当前使用: <code>customStack</code> (日志 + 计时 + 监控 + 默认栈)</p>
<a class="api-link" href="interceptor_api">使用 API 限流栈</a>
<a class="api-link" href="interceptor_api">切换到 API 限流栈</a>
<a class="api-link" href="interceptor">恢复默认栈</a>
<p>API 栈包含: 日志 + 限流 + 验证 + 计时 + 默认栈</p>
<table>
<tr><th>拦截器</th><th>作用</th><th>你应该观察什么</th></tr>
<tr><td>LoggingInterceptor</td><td>记录请求进入/退出</td><td>看日志顺序,理解责任链</td></tr>
<tr><td>TimingInterceptor</td><td>统计执行耗时</td><td>看 executionTime 如何注入 request</td></tr>
<tr><td>MonitorInterceptor</td><td>累积 Action 调用统计</td><td>连续刷新看调用数增长</td></tr>
<tr><td>RateLimitInterceptor</td><td>限制频率</td><td>API 栈里观察限流触发</td></tr>
<tr><td>ValidationInterceptor</td><td>校验输入</td><td>思考错误应该在哪一层拦截</td></tr>
</table>
</div>
<div class="card">
<h3>🧪 快速实验建议</h3>
<ul>
<li>先刷新当前页两次,看调用统计是否增加</li>
<li>再切到 <code>interceptor_api</code>,对比多了哪些拦截器</li>
<li>观察“先进入后退出”的洋葱模型</li>
<li>思考:如果你要加鉴权,应放在哪个拦截器位置最合理?</li>
</ul>
</div>
<div class="card">
<h3>📚 拦截器执行顺序</h3>
<pre>

View File

@@ -62,6 +62,11 @@
<p>通过用户管理体验列表页、表单页、重定向和状态更新。</p>
<a class="btn btn-green" href="user">打开用户管理</a>
</div>
<div class="card">
<h3>🧪 OGNL / 参数绑定实验</h3>
<p>通过真正可交互的表单观察 Struts2 的字段绑定、嵌套对象绑定和集合绑定。</p>
<a class="btn btn-purple" href="ognl">打开 OGNL 实验室</a>
</div>
</div>
<h2>📚 学习路径</h2>

View File

@@ -0,0 +1,83 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>OGNL / 参数绑定实验室 - Struts2</title>
<style>
body { font-family: Arial, sans-serif; max-width: 1000px; margin: 40px auto; padding: 20px; background:#f7f8fa; }
h1 { color:#8e44ad; }
.card { background:#fff; border-radius:12px; padding:20px; margin:18px 0; box-shadow:0 4px 14px rgba(0,0,0,.06); }
.lab { background:#fff7e6; border-left:4px solid #fa8c16; padding:15px; border-radius:8px; margin:20px 0; }
.form-row { display:flex; gap:12px; flex-wrap:wrap; margin:10px 0; }
input, select { padding:10px; min-width:180px; }
button { background:#8e44ad; color:#fff; border:none; padding:10px 18px; border-radius:6px; cursor:pointer; }
table { width:100%; border-collapse:collapse; margin-top:12px; }
th, td { border:1px solid #ddd; padding:10px; text-align:left; }
th { background:#8e44ad; color:white; }
code { background:#f0f0f0; padding:2px 6px; border-radius:4px; }
</style>
</head>
<body>
<h1>🧪 OGNL / 参数绑定实验室</h1>
<div class="lab">
<strong>实验任务卡</strong>
<ul>
<li>输入关键字和最小年龄,观察 Struts2 如何自动绑定请求参数到 Action 字段</li>
<li>注意 <code>keyword</code>、<code>minAge</code>、<code>user.name</code> 这类命名会如何映射</li>
<li>观察页面里 <code>&lt;s:property&gt;</code>、<code>&lt;s:iterator&gt;</code> 如何读取 Action 中的数据</li>
</ul>
</div>
<div class="card">
<h3>参数过滤实验</h3>
<s:form action="ognl_bind" method="post">
<div class="form-row">
<s:textfield name="keyword" label="关键字" placeholder="例如:张 / example"/>
<s:textfield name="minAge" label="最小年龄" placeholder="例如20"/>
</div>
<div class="form-row">
<s:textfield name="user.name" label="嵌套对象 user.name" placeholder="例如:测试用户"/>
<s:checkboxlist name="tags" label="兴趣标签" list="{'MVC','OGNL','Interceptor','JSP'}"/>
</div>
<button type="submit">执行绑定与过滤</button>
</s:form>
</div>
<div class="card">
<h3>绑定结果观察</h3>
<p>keyword = <code><s:property value="keyword" default="(空)"/></code></p>
<p>minAge = <code><s:property value="minAge" default="(空)"/></code></p>
<p>user.name = <code><s:property value="user.name" default="(空)"/></code></p>
<p>tags = <code><s:property value="tags" default="[]"/></code></p>
</div>
<div class="card">
<h3>过滤后的用户列表</h3>
<table>
<tr><th>姓名</th><th>邮箱</th><th>年龄</th></tr>
<s:iterator value="filteredUsers" var="u">
<tr>
<td><s:property value="#u.name"/></td>
<td><s:property value="#u.email"/></td>
<td><s:property value="#u.age"/></td>
</tr>
</s:iterator>
</table>
</div>
<div class="card">
<h3>学习点</h3>
<ul>
<li><code>keyword</code> → 直接绑定到 Action 同名字段</li>
<li><code>user.name</code> → 绑定到嵌套对象属性</li>
<li><code>tags</code> → 多选值绑定到 <code>List&lt;String&gt;</code></li>
<li><code>&lt;s:iterator&gt;</code> 会遍历 Action 暴露出来的集合</li>
</ul>
</div>
<p><a href="learn">← 返回学习中心</a></p>
</body>
</html>