Files
2026-03-18 15:18:41 +08:00

203 lines
10 KiB
HTML
Raw Permalink 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>IoC 生命周期可视化实验室 - Spring Boot</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px 20px; text-align: center; margin-bottom: 20px; border-radius: 10px; }
.header h1 { font-size: 2em; }
.nav { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; justify-content: center; }
.nav a { padding: 10px 20px; background: white; border-radius: 20px; text-decoration: none; color: #333; font-size: 0.9em; }
.nav a:hover, .nav a.active { background: #667eea; color: white; }
.card { background: white; border-radius: 10px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.08); }
.card h3 { color: #667eea; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(320px,1fr)); gap:16px; }
.concept-box { background: #f8f9fa; border-left: 4px solid #667eea; padding: 15px; margin: 10px 0; border-radius: 5px; }
.lab { background:#fff7e6; border-left:4px solid #fa8c16; padding:15px; border-radius:8px; margin-bottom:20px; }
.btn { padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; margin: 5px; }
.btn:hover { background: #5a6fd6; }
.btn-secondary { background: #6c757d; }
.btn-purple { background:#8e44ad; }
.result { background: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: 5px; margin-top: 10px; font-family: monospace; font-size: 0.9em; overflow-x: auto; max-height: 420px; overflow-y: auto; white-space: pre-wrap; }
table { width: 100%; border-collapse: collapse; margin: 10px 0; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; }
tr:hover { background: #f8f9fa; }
.badge { padding: 4px 8px; border-radius: 4px; font-size: 0.8em; }
.badge-primary { background: #667eea; color: white; }
.badge-success { background: #28a745; color: white; }
.badge-warning { background: #ffc107; color: #333; }
.timeline { background:#0f172a; color:#e2e8f0; padding:14px; border-radius:8px; max-height:420px; overflow:auto; }
.timeline-item { padding:10px; border-left:3px solid #60a5fa; margin:10px 0; background:rgba(255,255,255,0.04); }
.small { color:#666; font-size:0.9em; line-height:1.7; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📦 IoC 生命周期可视化实验室</h1>
<p>把“类加载”“Bean 创建”“单例/多例/懒加载”拆开看,自己动手触发、自己观察结果。</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="ioc.html" class="active">📦 IoC</a>
<a href="verify-lab.html">🩺 修复验证</a>
<a href="aop.html">🔪 AOP</a>
<a href="mybatis.html">💾 MyBatis</a>
<a href="transaction.html">🔄 事务</a>
<a href="api.html">🔌 API</a>
</div>
<div class="lab">
<strong>🧪 实验任务卡</strong>
<ul style="padding-left:20px; line-height:1.8; margin-top:8px;">
<li>先点“加载总览”,搞清楚 JVM 类加载 和 Spring Bean 生命周期不是一回事</li>
<li>再点“比较三种作用域”,对比 singleton / prototype / lazy singleton 的实例 ID 和 hashCode</li>
<li>然后分别连续点击“获取单例 / 获取多例 / 获取懒加载单例”,观察时间线变化</li>
<li>最后重置时间线,再重新做一遍,看看第一次触发和第二次触发的区别</li>
</ul>
</div>
<div class="grid">
<div class="card">
<h3>📚 核心概念总览</h3>
<button class="btn" onclick="loadOverview()">加载总览</button>
<div id="overviewResult" class="result">点击按钮查看“类加载 vs Bean 生命周期”...</div>
</div>
<div class="card">
<h3>🆚 作用域对比实验</h3>
<button class="btn" onclick="compareScopes()">比较三种作用域</button>
<button class="btn btn-secondary" onclick="resetTimeline()">重置时间线</button>
<div class="small">观察重点singleton 两次获取是否同一实例prototype 为什么每次都变lazy singleton 第一次获取前是否就已经创建?</div>
<div id="compareResult" class="result"></div>
</div>
</div>
<div class="card">
<h3>🎯 单点触发实验</h3>
<button class="btn" onclick="inspect('singleton')">获取单例 Bean</button>
<button class="btn btn-purple" onclick="inspect('prototype')">获取多例 Bean</button>
<button class="btn btn-secondary" onclick="inspect('lazy')">获取懒加载单例</button>
<div id="inspectResult" class="result">点击上面的按钮,观察实例 ID / hashCode / 访问次数如何变化...</div>
</div>
<div class="grid">
<div class="card">
<h3>🕰️ 生命周期时间线</h3>
<button class="btn" onclick="loadTimeline()">刷新时间线</button>
<div id="timelineResult" class="timeline">时间线加载中...</div>
</div>
<div class="card">
<h3>📊 Bean 作用域解释</h3>
<table>
<tr><th>作用域</th><th>创建时机</th><th>实例特点</th></tr>
<tr><td><span class="badge badge-primary">singleton</span></td><td>通常容器启动时</td><td>全局一个实例,反复获取同一个对象</td></tr>
<tr><td><span class="badge badge-success">prototype</span></td><td>每次 getBean 时</td><td>每次都是新实例,不进单例池</td></tr>
<tr><td><span class="badge badge-warning">lazy singleton</span></td><td>第一次真正使用时</td><td>启动不创建,首次获取才创建,之后复用</td></tr>
</table>
<div class="concept-box">
<h4>记住这句话</h4>
<p><strong>类加载 ≠ Bean 创建Bean 创建 ≠ 每次请求都 new。</strong></p>
</div>
</div>
</div>
<div class="card">
<h3>🔍 查看所有 Bean</h3>
<button class="btn" onclick="loadBeans()">刷新 Bean 列表</button>
<button class="btn btn-secondary" onclick="document.getElementById('beansResult').innerHTML=''">清空</button>
<div id="beansResult" class="result"></div>
</div>
</div>
<script>
async function loadOverview() {
const result = document.getElementById('overviewResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/ioc/lifecycle/overview');
const data = await res.json();
result.textContent = JSON.stringify(data, null, 2);
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function inspect(scope) {
const result = document.getElementById('inspectResult');
result.textContent = '触发中...';
try {
const res = await fetch(`/api/learning/ioc/lifecycle/inspect/${scope}?trigger=ui-click`);
const data = await res.json();
result.textContent = JSON.stringify(data, null, 2);
loadTimeline();
} catch (e) {
result.textContent = '触发失败: ' + e.message;
}
}
async function compareScopes() {
const result = document.getElementById('compareResult');
result.textContent = '对比中...';
try {
const res = await fetch('/api/learning/ioc/lifecycle/compare');
const data = await res.json();
result.textContent = JSON.stringify(data, null, 2);
loadTimeline();
} catch (e) {
result.textContent = '对比失败: ' + e.message;
}
}
async function loadTimeline() {
const result = document.getElementById('timelineResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/ioc/lifecycle/timeline');
const data = await res.json();
if (!data.length) {
result.textContent = '暂无时间线事件,先点上面的实验按钮。';
return;
}
result.innerHTML = data.map(item => `
<div class="timeline-item">
<div><strong>#${item.seq}</strong> [${item.scope}] ${item.beanName}</div>
<div>${item.phase}</div>
<div style="color:#94a3b8; margin-top:6px;">${item.detail}</div>
</div>
`).join('');
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function resetTimeline() {
await fetch('/api/learning/ioc/lifecycle/reset', { method: 'POST' });
loadTimeline();
}
async function loadBeans() {
const result = document.getElementById('beansResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/ioc/beans');
const data = await res.json();
result.innerHTML = `<strong>总 Bean 数: ${data.total}</strong>\n\nIoC 学习相关 Bean:\n${data.userBeans.map(b => ' - ' + b).join('\n')}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
loadOverview();
loadTimeline();
</script>
</body>
</html>