Files
springboot-demo/target/classes/static/aop.html
2026-03-23 13:05:44 +08:00

350 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spring AOP Lab</title>
<style>
: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; }
}
</style>
</head>
<body>
<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>
</div>
</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>
</div>
</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;
}
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];
}
});
}
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";
}
}
async function renderRequest(path, options) {
const box = document.getElementById("resultBox");
const text = pageText();
box.textContent = text.loadingPrefix + path + " ...";
box.dataset.state = "loading";
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";
}
}
async function loadUsers() {
await renderRequest("/api/users");
}
async function loadUserStats() {
await renderRequest("/api/users/stats");
}
async function loadAopStats() {
await renderRequest("/aop/aop/stats");
}
async function sendInvalidUser() {
await renderRequest("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "", email: "bad", age: 999 })
});
}
window.learningShell.mountShell({ onLanguageChange: renderLanguage });
renderLanguage();
</script>
</body>
</html>