2026-03-07 05:43:15 +00:00
<!DOCTYPE html>
< html lang = "zh-CN" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
2026-03-23 13:05:44 +08:00
< title > Spring AOP Lab< / title >
2026-03-07 05:43:15 +00:00
< style >
2026-03-23 13:05:44 +08:00
:root {
--bg: #eef7ef;
--panel: rgba(255,255,255,0.95);
--line: #d8e7d8;
--text: #102033;
--muted: #5c7184;
--brand: #3f8f2c;
--accent: #0f67b5;
--shadow: 0 20px 48px rgba(16, 32, 51, 0.12);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
color: var(--text);
background:
radial-gradient(circle at top right, rgba(63, 143, 44, 0.14), transparent 30%),
radial-gradient(circle at bottom left, rgba(15, 103, 181, 0.1), transparent 24%),
var(--bg);
}
.page { max-width: 1280px; margin: 0 auto; padding: 24px; }
.hero, .card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 28px;
box-shadow: var(--shadow);
}
.hero { padding: 28px; margin-bottom: 18px; }
.eyebrow {
display: inline-flex;
padding: 7px 12px;
border-radius: 999px;
background: rgba(63, 143, 44, 0.1);
color: var(--brand);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.1em;
text-transform: uppercase;
}
h1, h2, h3 { margin: 10px 0 12px; }
p { margin: 0; color: var(--muted); line-height: 1.8; }
.actions, .toolbar, .flow, .chips {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn, .btn-soft {
border: 0;
border-radius: 999px;
padding: 12px 18px;
font-weight: 700;
cursor: pointer;
}
.btn { background: linear-gradient(135deg, var(--brand), #62b049); color: #fff; }
.btn-soft { background: #eaf3ff; color: var(--accent); }
.grid {
display: grid;
gap: 18px;
grid-template-columns: 420px minmax(0, 1fr);
}
.card { padding: 22px; }
.list {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 14px;
}
.list-item, .flow-step {
padding: 14px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.45);
}
.flow {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
margin-top: 14px;
}
.pill {
display: inline-flex;
padding: 6px 10px;
border-radius: 999px;
background: rgba(15, 103, 181, 0.08);
color: var(--accent);
font-size: 12px;
font-weight: 700;
}
pre {
margin: 0;
min-height: 280px;
padding: 16px;
border-radius: 18px;
background: #0f1621;
color: #deebff;
white-space: pre-wrap;
font-family: Consolas, "Courier New", monospace;
overflow: auto;
}
@media (max-width: 1060px) {
.grid, .flow { grid-template-columns: 1fr; }
}
@media (max-width: 720px) {
.page { padding: 14px; }
}
2026-03-07 05:43:15 +00:00
< / style >
< / head >
< body >
2026-03-23 13:05:44 +08:00
< div class = "page" >
< section class = "hero" >
< div class = "eyebrow" data-i18n = "heroBadge" > < / div >
< h1 data-i18n = "heroTitle" > < / h1 >
< p data-i18n = "heroText" > < / p >
< div class = "actions" style = "margin-top:18px;" >
< a class = "btn-soft" href = "/access.html" data-i18n = "loginLink" > < / a >
< a class = "btn-soft" href = "/" data-i18n = "homeLink" > < / a >
< a class = "btn-soft" href = "/users.html" data-i18n = "usersLink" > < / a >
< a class = "btn-soft" href = "/events.html" data-i18n = "eventsLink" > < / a >
2026-03-07 05:43:15 +00:00
< / div >
2026-03-23 13:05:44 +08:00
< / section >
< div class = "grid" >
< aside class = "card" >
< div class = "eyebrow" data-i18n = "pathBadge" > < / div >
< h2 data-i18n = "pathTitle" > < / h2 >
< div class = "list" >
< div class = "list-item" >
< strong data-i18n = "step1Title" > < / strong >
< p data-i18n = "step1Text" > < / p >
< / div >
< div class = "list-item" >
< strong data-i18n = "step2Title" > < / strong >
< p data-i18n = "step2Text" > < / p >
< / div >
< div class = "list-item" >
< strong data-i18n = "step3Title" > < / strong >
< p data-i18n = "step3Text" > < / p >
< / div >
< / div >
< div class = "eyebrow" style = "margin-top:18px;" data-i18n = "adviceBadge" > < / div >
< div class = "chips" style = "margin-top:12px;" >
< span class = "pill" > @Before< / span >
< span class = "pill" > @After< / span >
< span class = "pill" > @AfterReturning< / span >
< span class = "pill" > @AfterThrowing< / span >
< span class = "pill" > @Around< / span >
< / div >
< div class = "flow" >
< div class = "flow-step" > < strong data-i18n = "flow1Title" > < / strong > < p data-i18n = "flow1Text" > < / p > < / div >
< div class = "flow-step" > < strong data-i18n = "flow2Title" > < / strong > < p data-i18n = "flow2Text" > < / p > < / div >
< div class = "flow-step" > < strong data-i18n = "flow3Title" > < / strong > < p data-i18n = "flow3Text" > < / p > < / div >
< div class = "flow-step" > < strong data-i18n = "flow4Title" > < / strong > < p data-i18n = "flow4Text" > < / p > < / div >
< div class = "flow-step" > < strong data-i18n = "flow5Title" > < / strong > < p data-i18n = "flow5Text" > < / p > < / div >
< / div >
< / aside >
< main class = "card" >
< div class = "eyebrow" data-i18n = "liveBadge" > < / div >
< h2 data-i18n = "liveTitle" > < / h2 >
< p data-i18n = "liveText" > < / p >
< div class = "toolbar" style = "margin-top:14px;" >
< button class = "btn" type = "button" onclick = "loadUsers()" data-i18n = "loadUsersButton" > < / button >
< button class = "btn-soft" type = "button" onclick = "loadUserStats()" data-i18n = "loadStatsButton" > < / button >
< button class = "btn-soft" type = "button" onclick = "sendInvalidUser()" data-i18n = "invalidUserButton" > < / button >
< button class = "btn-soft" type = "button" onclick = "loadAopStats()" data-i18n = "aopStatsButton" > < / button >
< / div >
< div class = "list" style = "margin-top:18px;" >
< div class = "list-item" >
< strong data-i18n = "observeTitle" > < / strong >
< p data-i18n = "observeText" > < / p >
< / div >
< div class = "list-item" >
< strong data-i18n = "codeTitle" > < / strong >
< p data-i18n = "codeText" > < / p >
< / div >
< / div >
< pre id = "resultBox" > < / pre >
< / main >
2026-03-07 05:43:15 +00:00
< / div >
2026-03-23 13:05:44 +08:00
< / div >
< script src = "/learning-shell.js" > < / script >
< script >
const I18N = {
zh: {
title: "Spring AOP 实验",
heroBadge: "AOP 实验",
heroTitle: "把横切行为从抽象概念变成可观察现象。",
heroText: "这一页专门展示学生最容易忽略的部分:通知是怎样包裹控制器和服务方法的、耗时如何被收集、以及校验失败为什么同样属于可观测的运行时行为。",
loginLink: "打开登录实验",
homeLink: "返回首页",
usersLink: "打开用户实验",
eventsLink: "打开事件实验",
pathBadge: "实验路径",
pathTitle: "推荐的 AOP 学习顺序",
step1Title: "1. 先触发控制器调用",
step1Text: "运行用户列表和统计接口,确保控制器和服务层方法都被执行到。",
step2Title: "2. 再触发一次校验失败",
step2Text: "发送一个非法用户载荷,对比失败请求和成功请求在切面统计上的表现。",
step3Title: "3. 最后查看切面输出",
step3Text: "加载统计接口,对比调用次数、总耗时和平均耗时。",
adviceBadge: "通知地图",
flow1Title: "Before",
flow1Text: "观察入参或鉴权上下文。",
flow2Title: "Around",
flow2Text: "开启计时器并继续执行连接点。",
flow3Title: "Controller",
flow3Text: "把业务交给服务层。",
flow4Title: "Service",
flow4Text: "执行具体业务规则。",
flow5Title: "After*",
flow5Text: "记录结果或失败细节。",
liveBadge: "实时运行区",
liveTitle: "发起请求并查看采集到的指标",
liveText: "下面的按钮会帮你制造真实流量,再直接查看切面输出,不需要再切换到别的工具。",
loadUsersButton: "加载用户",
loadStatsButton: "加载用户统计",
invalidUserButton: "发送非法用户",
aopStatsButton: "加载 AOP 统计",
observeTitle: "控制台里该观察什么",
observeText: "重点看控制器和服务层的耗时输出,对比成功路径和失败路径,理解 around 通知为什么仍然会记录方法执行时长。",
codeTitle: "代码里该先看什么",
codeText: "先读 PerformanceAspect, 再回头比对它包裹的用户接口。",
loadingPrefix: "正在加载 ",
requestFailedPrefix: "请求失败:",
placeholder: "请先运行上方任意实验,再查看实时 JSON 输出。"
},
en: {
title: "Spring AOP Lab",
heroBadge: "AOP Lab",
heroTitle: "Make cross-cutting behavior visible instead of abstract.",
heroText: "This page focuses on the parts students usually miss: where advice wraps controller and service methods, how timing is collected, and why a validation failure still counts as observable runtime behavior.",
loginLink: "Open login lab",
homeLink: "Back home",
usersLink: "Open user lab",
eventsLink: "Open event lab",
pathBadge: "Experiment Path",
pathTitle: "Recommended AOP sequence",
step1Title: "1. Trigger controller calls",
step1Text: "Run the list and stats endpoints so both controller and service methods execute.",
step2Title: "2. Trigger a validation failure",
step2Text: "Send an invalid user payload and compare the failed request with successful ones in the aspect metrics.",
step3Title: "3. Inspect aspect output",
step3Text: "Load the stats endpoint and compare call counts, total time, and average time.",
adviceBadge: "Advice Map",
flow1Title: "Before",
flow1Text: "Inspect inputs or auth context.",
flow2Title: "Around",
flow2Text: "Start the timer and continue the join point.",
flow3Title: "Controller",
flow3Text: "Delegate work to the service.",
flow4Title: "Service",
flow4Text: "Apply business logic.",
flow5Title: "After*",
flow5Text: "Record result or failure details.",
liveBadge: "Live Runs",
liveTitle: "Run requests and inspect collected metrics",
liveText: "The buttons below create real traffic and then surface the aspect output without switching tools.",
loadUsersButton: "Load users",
loadStatsButton: "Load user stats",
invalidUserButton: "Send invalid user",
aopStatsButton: "Load AOP stats",
observeTitle: "What to observe in the console",
observeText: "Look for controller and service timing lines. Compare success and failure paths to see why around advice still records method duration.",
codeTitle: "What to inspect in code",
codeText: "Read PerformanceAspect first, then compare it with the user endpoints that it wraps.",
loadingPrefix: "Loading ",
requestFailedPrefix: "Request failed: ",
placeholder: "Run one of the experiments above to inspect live JSON output."
}
};
function pageText() {
return I18N[window.learningShell.getLanguage()] || I18N.zh;
2026-03-07 05:43:15 +00:00
}
2026-03-23 13:05:44 +08:00
function applyTranslations(text) {
document.title = text.title;
document.querySelectorAll("[data-i18n]").forEach(function (element) {
const key = element.getAttribute("data-i18n");
if (Object.prototype.hasOwnProperty.call(text, key)) {
element.textContent = text[key];
}
});
2026-03-07 05:43:15 +00:00
}
2026-03-23 13:05:44 +08:00
function renderLanguage() {
const text = pageText();
applyTranslations(text);
const box = document.getElementById("resultBox");
if (!box.dataset.state || box.dataset.state === "idle") {
box.textContent = text.placeholder;
box.dataset.state = "idle";
2026-03-07 05:43:15 +00:00
}
}
2026-03-23 13:05:44 +08:00
async function renderRequest(path, options) {
const box = document.getElementById("resultBox");
const text = pageText();
box.textContent = text.loadingPrefix + path + " ...";
box.dataset.state = "loading";
2026-03-07 05:43:15 +00:00
2026-03-23 13:05:44 +08:00
try {
const data = await window.learningShell.requestJson(path, options || {});
box.textContent = JSON.stringify(data, null, 2);
box.dataset.state = "live";
} catch (error) {
box.textContent = text.requestFailedPrefix + window.learningShell.describeError(error);
box.dataset.state = "live";
}
}
2026-03-07 05:43:15 +00:00
2026-03-23 13:05:44 +08:00
async function loadUsers() {
await renderRequest("/api/users");
}
2026-03-07 05:43:15 +00:00
2026-03-23 13:05:44 +08:00
async function loadUserStats() {
await renderRequest("/api/users/stats");
}
2026-03-07 05:43:15 +00:00
2026-03-23 13:05:44 +08:00
async function loadAopStats() {
await renderRequest("/aop/aop/stats");
}
2026-03-18 15:18:30 +08:00
2026-03-23 13:05:44 +08:00
async function sendInvalidUser() {
await renderRequest("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "", email: "bad", age: 999 })
});
}
2026-03-18 15:18:30 +08:00
2026-03-23 13:05:44 +08:00
window.learningShell.mountShell({ onLanguageChange: renderLanguage });
renderLanguage();
< / script >
2026-03-07 05:43:15 +00:00
< / body >
2026-03-23 13:05:44 +08:00
< / html >