Update: cache module, security modules and build
This commit is contained in:
260
target/classes/static/advanced.html
Normal file
260
target/classes/static/advanced.html
Normal file
@@ -0,0 +1,260 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>高级功能学习 - 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, #f093fb 0%, #f5576c 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: #f5576c; 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: #f5576c; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
|
||||
|
||||
.config-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; }
|
||||
.config-item { background: #f8f9fa; padding: 15px; border-radius: 8px; border-left: 4px solid #f5576c; }
|
||||
.config-item h4 { margin-bottom: 10px; }
|
||||
.config-item .current { color: #28a745; font-weight: bold; }
|
||||
|
||||
.btn { padding: 10px 20px; background: #f5576c; color: white; border: none; border-radius: 5px; cursor: pointer; margin: 5px; }
|
||||
.btn:hover { background: #e0465b; }
|
||||
.btn-secondary { background: #6c757d; }
|
||||
|
||||
.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: 400px; overflow-y: auto; }
|
||||
|
||||
.compare-table { width: 100%; border-collapse: collapse; margin: 15px 0; }
|
||||
.compare-table th, .compare-table td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
|
||||
.compare-table th { background: #f8f9fa; }
|
||||
.compare-table tr:hover { background: #f8f9fa; }
|
||||
|
||||
.problem-box { background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 10px 0; border-radius: 5px; }
|
||||
.problem-box h4 { color: #856404; margin-bottom: 8px; }
|
||||
|
||||
.redis-types { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
|
||||
.redis-type { background: #e3f2fd; padding: 15px; border-radius: 8px; }
|
||||
.redis-type h4 { color: #1976d2; margin-bottom: 5px; }
|
||||
.tipbox { background:#fff7e6;border-left:4px solid #fa8c16;padding:15px;border-radius:8px;margin-bottom:20px; }
|
||||
.tipbox h4 { color:#ad6800;margin-bottom:8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🚀 高级功能学习</h1>
|
||||
<p>Redis 缓存 | 分布式锁 | 多数据库 | 认证方案</p>
|
||||
</div>
|
||||
|
||||
<div class="nav">
|
||||
<a href="index.html">🏠 首页</a>
|
||||
<a href="ioc.html">📦 IoC</a>
|
||||
<a href="aop.html">🔪 AOP</a>
|
||||
<a href="mybatis.html">💾 MyBatis</a>
|
||||
<a href="transaction.html">🔄 事务</a>
|
||||
<a href="users.html">👥 用户</a>
|
||||
<a href="advanced.html" class="active">🚀 高级</a>
|
||||
<a href="auth-lab.html">🔐 鉴权实验室</a>
|
||||
<a href="api.html">🔌 API</a>
|
||||
</div>
|
||||
|
||||
<div class="tipbox">
|
||||
<h4>🧪 实验任务卡(高级模块)</h4>
|
||||
<label style="display:block;margin-bottom:8px;"><input id="advancedTaskDone" type="checkbox" onchange="toggleAdvancedTaskDone(this)"> 本任务我已经完成</label>
|
||||
<ul style="padding-left:20px;line-height:1.8;">
|
||||
<li>目标:比较 learn/advanced profile 下可用能力差异</li>
|
||||
<li>步骤1:先查看“系统配置”和“认证方案对比”</li>
|
||||
<li>步骤2:执行 Redis SET/GET + 分布式锁接口</li>
|
||||
<li>预期:advanced 模式下功能更完整,返回字段更丰富</li>
|
||||
<li>常见坑:本机无 Redis 导致接口失败(属于环境问题)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>⚙️ 系统配置</h3>
|
||||
<button class="btn" onclick="loadConfig()">查看当前配置</button>
|
||||
<div id="configResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🔐 认证方案对比</h3>
|
||||
<button class="btn" onclick="loadAuthCompare()">JWT vs Sa-Token</button>
|
||||
<a class="btn btn-secondary" href="auth-lab.html">进入鉴权实验室</a>
|
||||
<div id="authResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>💾 Redis 数据类型</h3>
|
||||
<div class="redis-types">
|
||||
<div class="redis-type"><h4>String</h4><p>字符串,最基本类型</p></div>
|
||||
<div class="redis-type"><h4>Hash</h4><p>哈希,存储对象</p></div>
|
||||
<div class="redis-type"><h4>List</h4><p>列表,队列/栈</p></div>
|
||||
<div class="redis-type"><h4>Set</h4><p>集合,去重运算</p></div>
|
||||
<div class="redis-type"><h4>SortedSet</h4><p>有序集合,排名</p></div>
|
||||
</div>
|
||||
<button class="btn" onclick="loadRedisTypes()">查看详细说明</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🧪 Redis 操作测试</h3>
|
||||
<div style="display:flex;gap:10px;margin-bottom:15px;">
|
||||
<input type="text" id="redisKey" placeholder="Key" style="flex:1;padding:10px;border:1px solid #ddd;border-radius:5px;">
|
||||
<input type="text" id="redisValue" placeholder="Value" style="flex:1;padding:10px;border:1px solid #ddd;border-radius:5px;">
|
||||
<input type="number" id="redisTtl" placeholder="TTL(秒)" value="60" style="width:100px;padding:10px;border:1px solid #ddd;border-radius:5px;">
|
||||
</div>
|
||||
<button class="btn" onclick="setRedisString()">SET</button>
|
||||
<button class="btn btn-secondary" onclick="getRedisString()">GET</button>
|
||||
<div id="redisResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🔒 分布式锁演示</h3>
|
||||
<p>模拟多个请求竞争同一资源</p>
|
||||
<input type="text" id="lockResource" placeholder="资源名称" value="order:1001" style="padding:10px;border:1px solid #ddd;border-radius:5px;margin-right:10px;">
|
||||
<button class="btn" onclick="testDistributedLock()">获取分布式锁</button>
|
||||
<div id="lockResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>⚠️ 缓存三大问题</h3>
|
||||
<div class="problem-box">
|
||||
<h4>缓存穿透</h4>
|
||||
<p><strong>问题:</strong>查询不存在的数据,每次都打到数据库</p>
|
||||
<p><strong>解决:</strong>布隆过滤器 | 缓存空值</p>
|
||||
</div>
|
||||
<div class="problem-box">
|
||||
<h4>缓存击穿</h4>
|
||||
<p><strong>问题:</strong>热点key过期,大量请求打到数据库</p>
|
||||
<p><strong>解决:</strong>互斥锁 | 逻辑过期</p>
|
||||
</div>
|
||||
<div class="problem-box">
|
||||
<h4>缓存雪崩</h4>
|
||||
<p><strong>问题:</strong>大量key同时过期,数据库压力激增</p>
|
||||
<p><strong>解决:</strong>随机过期时间 | 多级缓存</p>
|
||||
</div>
|
||||
<button class="btn" onclick="loadCacheProblems()">查看详细方案</button>
|
||||
<div id="cacheResult" class="result"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ADV_TASK_KEY = 'task.advanced.done';
|
||||
|
||||
function toggleAdvancedTaskDone(el) {
|
||||
localStorage.setItem(ADV_TASK_KEY, el.checked ? '1' : '0');
|
||||
}
|
||||
|
||||
function initAdvancedTaskState() {
|
||||
const done = localStorage.getItem(ADV_TASK_KEY) === '1';
|
||||
const checkbox = document.getElementById('advancedTaskDone');
|
||||
if (checkbox) checkbox.checked = done;
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
const result = document.getElementById('configResult');
|
||||
result.textContent = '加载中...';
|
||||
try {
|
||||
const res = await fetch('/api/learning/advanced/config');
|
||||
const data = await res.json();
|
||||
result.innerHTML = `<strong>系统配置</strong>\n\n${JSON.stringify(data, null, 2)}`;
|
||||
} catch (e) {
|
||||
result.textContent = '加载失败: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAuthCompare() {
|
||||
const result = document.getElementById('authResult');
|
||||
result.textContent = '加载中...';
|
||||
try {
|
||||
const res = await fetch('/api/learning/advanced/auth/compare');
|
||||
const data = await res.json();
|
||||
result.innerHTML = `<strong>认证方案对比</strong>\n\n${JSON.stringify(data, null, 2)}`;
|
||||
} catch (e) {
|
||||
result.textContent = '加载失败: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRedisTypes() {
|
||||
const result = document.getElementById('redisResult');
|
||||
try {
|
||||
const res = await fetch('/api/learning/advanced/redis/types');
|
||||
const data = await res.json();
|
||||
result.innerHTML = `<strong>Redis 数据类型</strong>\n\n${JSON.stringify(data, null, 2)}`;
|
||||
} catch (e) {
|
||||
result.textContent = '加载失败: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function setRedisString() {
|
||||
const key = document.getElementById('redisKey').value;
|
||||
const value = document.getElementById('redisValue').value;
|
||||
const ttl = document.getElementById('redisTtl').value;
|
||||
|
||||
if (!key || !value) {
|
||||
alert('请输入 Key 和 Value');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = document.getElementById('redisResult');
|
||||
try {
|
||||
const res = await fetch(`/api/learning/advanced/redis/string?key=${encodeURIComponent(key)}&value=${encodeURIComponent(value)}&ttl=${ttl}`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
result.innerHTML = `<strong>SET 结果</strong>\n\n${JSON.stringify(data, null, 2)}`;
|
||||
} catch (e) {
|
||||
result.textContent = '操作失败: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function getRedisString() {
|
||||
const key = document.getElementById('redisKey').value;
|
||||
if (!key) {
|
||||
alert('请输入 Key');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = document.getElementById('redisResult');
|
||||
try {
|
||||
const res = await fetch(`/api/learning/advanced/redis/string?key=${encodeURIComponent(key)}`);
|
||||
const data = await res.json();
|
||||
result.innerHTML = `<strong>GET 结果</strong>\n\n${JSON.stringify(data, null, 2)}`;
|
||||
} catch (e) {
|
||||
result.textContent = '操作失败: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function testDistributedLock() {
|
||||
const resource = document.getElementById('lockResource').value;
|
||||
const result = document.getElementById('lockResult');
|
||||
result.textContent = '获取锁中...';
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/learning/advanced/redis/lock?resource=${encodeURIComponent(resource)}`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
result.innerHTML = `<strong>分布式锁结果</strong>\n\n${JSON.stringify(data, null, 2)}`;
|
||||
} catch (e) {
|
||||
result.textContent = '操作失败: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCacheProblems() {
|
||||
const result = document.getElementById('cacheResult');
|
||||
try {
|
||||
const res = await fetch('/api/learning/advanced/cache/problems');
|
||||
const data = await res.json();
|
||||
result.innerHTML = `<strong>缓存问题解决方案</strong>\n\n${JSON.stringify(data, null, 2)}`;
|
||||
} catch (e) {
|
||||
result.textContent = '加载失败: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
loadConfig();
|
||||
initAdvancedTaskState();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -49,6 +49,8 @@
|
||||
.tab-content.active { display: block; }
|
||||
|
||||
.json-input { width: 100%; min-height: 100px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-family: monospace; font-size: 0.9em; }
|
||||
.profile-banner { background:#fff7e6;border-left:4px solid #fa8c16;padding:12px 14px;border-radius:8px;margin-bottom:18px; color:#874d00; }
|
||||
.tools { margin: 10px 0 16px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -68,6 +70,12 @@
|
||||
<a href="api.html" class="active">🔌 API</a>
|
||||
</div>
|
||||
|
||||
<div id="profileBanner" class="profile-banner">正在读取 profile...</div>
|
||||
<div class="tools">
|
||||
<button class="btn btn-primary" onclick="copyCurl()">复制当前示例 cURL(GET /api/users)</button>
|
||||
<a class="btn btn-primary" href="verify-lab.html">进入修复验证实验室</a>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab active" onclick="switchTab('user')">👥 用户 API</div>
|
||||
<div class="tab" onclick="switchTab('product')">📦 产品 API</div>
|
||||
@@ -248,6 +256,27 @@
|
||||
document.getElementById(tab + 'Tab').classList.add('active');
|
||||
}
|
||||
|
||||
async function loadProfileBanner() {
|
||||
try {
|
||||
const res = await fetch('/api/profile');
|
||||
const data = await res.json();
|
||||
const enabled = (data.enabledModules || []).join(', ');
|
||||
document.getElementById('profileBanner').textContent = `当前 profile: ${data.profile} | 鉴权模式: ${data.authType || 'none'} | 已启用模块: ${enabled}`;
|
||||
} catch (e) {
|
||||
document.getElementById('profileBanner').textContent = '当前 profile 读取失败,请检查 /api/profile';
|
||||
}
|
||||
}
|
||||
|
||||
async function copyCurl() {
|
||||
const curl = `curl -X GET "${window.location.origin}/api/users"`;
|
||||
try {
|
||||
await navigator.clipboard.writeText(curl);
|
||||
alert('已复制: ' + curl);
|
||||
} catch (e) {
|
||||
alert('复制失败,请手动复制: ' + curl);
|
||||
}
|
||||
}
|
||||
|
||||
async function testApi(method, url, body, resultId) {
|
||||
const resultDiv = document.getElementById(resultId);
|
||||
resultDiv.classList.add('show');
|
||||
@@ -275,6 +304,8 @@
|
||||
resultDiv.innerHTML = `<strong style="color:#ff6b6b;">Error</strong>\n\n${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
loadProfileBanner();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
142
target/classes/static/auth-lab.html
Normal file
142
target/classes/static/auth-lab.html
Normal file
@@ -0,0 +1,142 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>鉴权实验室 - 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: 1100px; margin: 0 auto; padding: 20px; }
|
||||
.header { background: linear-gradient(135deg,#4facfe,#00f2fe); color:#fff; padding:28px 20px; border-radius:12px; margin-bottom:20px; text-align:center; }
|
||||
.nav { display:flex; gap:10px; flex-wrap:wrap; justify-content:center; margin-bottom:20px; }
|
||||
.nav a { padding:10px 18px; background:#fff; border-radius:20px; text-decoration:none; color:#333; }
|
||||
.nav a.active { background:#4facfe; color:#fff; }
|
||||
.card { background:#fff; border-radius:12px; padding:20px; margin-bottom:20px; box-shadow:0 2px 10px rgba(0,0,0,.08); }
|
||||
.card h3 { color:#1890ff; margin-bottom:12px; }
|
||||
.lab { background:#fff7e6; border-left:4px solid #fa8c16; padding:15px; border-radius:8px; margin-bottom:20px; }
|
||||
.row { display:flex; gap:10px; flex-wrap:wrap; margin:10px 0; }
|
||||
input, select { padding:10px; border:1px solid #ddd; border-radius:6px; min-width:200px; }
|
||||
button { padding:10px 16px; background:#1890ff; color:#fff; border:none; border-radius:6px; cursor:pointer; }
|
||||
button.secondary { background:#6c757d; }
|
||||
.result { background:#111827; color:#d1d5db; padding:14px; border-radius:8px; min-height:120px; white-space:pre-wrap; font-family:monospace; }
|
||||
.hint { color:#666; margin-top:8px; line-height:1.7; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🔐 Spring 鉴权实验室</h1>
|
||||
<p>登录 → 取 Token → 访问受保护接口 → 对比 learn/advanced 模式</p>
|
||||
</div>
|
||||
|
||||
<div class="nav">
|
||||
<a href="index.html">🏠 首页</a>
|
||||
<a href="advanced.html">🚀 高级功能</a>
|
||||
<a href="verify-lab.html">🩺 修复验证实验室</a>
|
||||
<a href="api.html">🔌 API 测试</a>
|
||||
<a href="auth-lab.html" class="active">🔐 鉴权实验室</a>
|
||||
</div>
|
||||
|
||||
<div class="lab">
|
||||
<strong>实验任务卡</strong>
|
||||
<ul style="padding-left:20px; line-height:1.8; margin-top:8px;">
|
||||
<li>步骤1:点击“读取当前模式”,确认现在是 learn / advanced</li>
|
||||
<li>步骤2:用 admin/admin123 登录,拿到 token</li>
|
||||
<li>步骤3:带上 token 调用当前实验接口,观察是否放行或返回 503/401</li>
|
||||
<li>步骤4:切换 auth.type 后对比 none/jwt/satoken 行为差异</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>当前模式</h3>
|
||||
<div class="row">
|
||||
<button onclick="loadProfile()">读取当前模式</button>
|
||||
</div>
|
||||
<div id="profileResult" class="result">点击按钮读取...</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>登录拿 Token</h3>
|
||||
<div class="row">
|
||||
<input id="username" value="admin" placeholder="用户名">
|
||||
<input id="password" value="admin123" placeholder="密码" type="password">
|
||||
<button onclick="login()">登录</button>
|
||||
</div>
|
||||
<div class="hint">演示账号:admin/admin123 或 user/user123</div>
|
||||
<div id="loginResult" class="result">点击登录后显示响应...</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>接口访问实验</h3>
|
||||
<div class="row">
|
||||
<button onclick="callSecureApi()">访问 /api/auth/info</button>
|
||||
<button class="secondary" onclick="clearToken()">清空本地 Token</button>
|
||||
</div>
|
||||
<div class="hint">当前页面会优先从 localStorage 读取 token 并自动加到 Authorization 头里。learn 模式下接口通常直接放行;advanced+jwt 下更适合观察真实鉴权链路。</div>
|
||||
<div id="secureResult" class="result">点击按钮后显示接口结果...</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let cachedToken = localStorage.getItem('spring.scaffold.token') || '';
|
||||
|
||||
async function loadProfile() {
|
||||
const el = document.getElementById('profileResult');
|
||||
el.textContent = '读取中...';
|
||||
try {
|
||||
const res = await fetch('/api/profile');
|
||||
const data = await res.json();
|
||||
el.textContent = JSON.stringify(data, null, 2);
|
||||
} catch (e) {
|
||||
el.textContent = '读取失败: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function login() {
|
||||
const el = document.getElementById('loginResult');
|
||||
el.textContent = '登录中...';
|
||||
try {
|
||||
const res = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value,
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
const token = data.token || data.data?.token || '';
|
||||
if (token) {
|
||||
cachedToken = token;
|
||||
localStorage.setItem('spring.scaffold.token', token);
|
||||
}
|
||||
el.textContent = JSON.stringify(data, null, 2);
|
||||
} catch (e) {
|
||||
el.textContent = '登录失败: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function callSecureApi() {
|
||||
const el = document.getElementById('secureResult');
|
||||
el.textContent = '请求中...';
|
||||
try {
|
||||
const headers = {};
|
||||
if (cachedToken) headers['Authorization'] = 'Bearer ' + cachedToken;
|
||||
const res = await fetch('/api/auth/info', { headers });
|
||||
const text = await res.text();
|
||||
el.textContent = `HTTP ${res.status}\n\n${text}`;
|
||||
} catch (e) {
|
||||
el.textContent = '请求失败: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
function clearToken() {
|
||||
cachedToken = '';
|
||||
localStorage.removeItem('spring.scaffold.token');
|
||||
document.getElementById('secureResult').textContent = '本地 token 已清空';
|
||||
}
|
||||
|
||||
loadProfile();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -41,6 +41,9 @@
|
||||
.status-item { background: white; padding: 20px 30px; border-radius: 10px; text-align: center; }
|
||||
.status-item .value { font-size: 2em; font-weight: bold; color: #667eea; }
|
||||
.status-item .label { color: #666; margin-top: 5px; }
|
||||
.lab { background:#fff7e6; border:1px solid #ffe58f; border-radius:10px; padding:16px; margin-bottom:20px; }
|
||||
.lab h4 { margin-bottom:8px; color:#ad6800; }
|
||||
.lab ul { margin-left:18px; color:#444; line-height:1.7; }
|
||||
|
||||
footer { text-align: center; padding: 30px; color: #666; margin-top: 40px; }
|
||||
</style>
|
||||
@@ -69,6 +72,22 @@
|
||||
<div class="value" id="orderCount">-</div>
|
||||
<div class="label">订单数量</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="value" id="activeProfile">-</div>
|
||||
<div class="label">当前 Profile</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lab">
|
||||
<h4>🧪 实验任务卡(事务模块)</h4>
|
||||
<p style="margin-bottom:8px;color:#8c8c8c;">鉴权学习建议:learn=none(先学核心),advanced=jwt/satoken(再学安全链路)</p>
|
||||
<ul>
|
||||
<li>目标:理解事务回滚与 REQUIRES_NEW 差异</li>
|
||||
<li>步骤1:到 transaction.html 创建普通订单(rollback=false)</li>
|
||||
<li>步骤2:再创建模拟失败订单(rollback=true)</li>
|
||||
<li>预期:主事务回滚,但独立事务可保留日志/部分数据(取决于实现)</li>
|
||||
<li>观察点:查看控制台事务日志(TransactionInterceptor TRACE)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav">
|
||||
@@ -78,6 +97,10 @@
|
||||
<a href="mybatis.html">💾 MyBatis</a>
|
||||
<a href="transaction.html">🔄 事务管理</a>
|
||||
<a href="users.html">👥 用户管理</a>
|
||||
<a href="advanced.html">🚀 高级功能</a>
|
||||
<a href="reflection.html">🪞 反射实验室</a>
|
||||
<a href="auth-lab.html">🔐 鉴权实验室</a>
|
||||
<a href="verify-lab.html">🩺 修复验证实验室</a>
|
||||
<a href="api.html">🔌 API 测试</a>
|
||||
</div>
|
||||
|
||||
@@ -142,6 +165,55 @@
|
||||
<a href="users.html" class="btn">开始学习 →</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🚀 高级功能</h3>
|
||||
<p>Redis 缓存、分布式锁、多数据库、认证方案对比,从小白到高手的进阶之路。</p>
|
||||
<ul class="feature-list">
|
||||
<li><span class="icon">✅</span>Redis 数据类型操作</li>
|
||||
<li><span class="icon">✅</span>缓存穿透/击穿/雪崩</li>
|
||||
<li><span class="icon">✅</span>分布式锁实现</li>
|
||||
<li><span class="icon">✅</span>JWT vs Sa-Token</li>
|
||||
<li><span class="icon">✅</span>多数据库切换</li>
|
||||
</ul>
|
||||
<a href="advanced.html" class="btn">进阶学习 →</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🪞 反射实验室</h3>
|
||||
<p>动态查看类结构、通过构造器创建对象、修改私有字段、调用私有方法,理解 Spring 等框架底层为什么依赖反射。</p>
|
||||
<ul class="feature-list">
|
||||
<li><span class="icon">✅</span>类/字段/方法元信息</li>
|
||||
<li><span class="icon">✅</span>反射构造对象</li>
|
||||
<li><span class="icon">✅</span>私有字段修改</li>
|
||||
<li><span class="icon">✅</span>公开/私有/静态方法调用</li>
|
||||
</ul>
|
||||
<a href="reflection.html" class="btn">开始实验 →</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🔐 鉴权实验室</h3>
|
||||
<p>一步步体验登录、带 Token 请求、鉴权放行/拦截,对比 learn / advanced 模式差异。</p>
|
||||
<ul class="feature-list">
|
||||
<li><span class="icon">✅</span>登录拿 Token</li>
|
||||
<li><span class="icon">✅</span>自动携带 Authorization</li>
|
||||
<li><span class="icon">✅</span>当前模式读取</li>
|
||||
<li><span class="icon">✅</span>交互式错误观察</li>
|
||||
</ul>
|
||||
<a href="auth-lab.html" class="btn">开始实验 →</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🩺 修复验证实验室</h3>
|
||||
<p>自己点检查,不靠口头确认。直接验证 profile、H2、用户服务、MyBatis 和鉴权模式。</p>
|
||||
<ul class="feature-list">
|
||||
<li><span class="icon">✅</span>一键总览检查</li>
|
||||
<li><span class="icon">✅</span>数据库链路验证</li>
|
||||
<li><span class="icon">✅</span>用户服务验证</li>
|
||||
<li><span class="icon">✅</span>MyBatis 查询验证</li>
|
||||
</ul>
|
||||
<a href="verify-lab.html" class="btn">开始验证 →</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🔌 API 测试面板</h3>
|
||||
<p>在线测试所有 API 接口,查看请求响应,理解 RESTful API 工作原理。</p>
|
||||
@@ -172,7 +244,7 @@
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>🍃 Spring Boot 学习脚手架 | <a href="/h2-console" target="_blank">H2 控制台</a> (JDBC: jdbc:h2:file:~/h2/springboot_scaffold, 用户: sa)</p>
|
||||
<p>🍃 Spring Boot 学习脚手架 | <a href="/h2-console" target="_blank">H2 控制台</a> (learn 模式 JDBC: jdbc:h2:mem:springboot_scaffold_learn, 用户: sa)</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -180,17 +252,19 @@
|
||||
// 加载状态数据
|
||||
async function loadStatus() {
|
||||
try {
|
||||
const [beans, users, products, orders] = await Promise.all([
|
||||
const [beans, users, products, orders, profile] = await Promise.all([
|
||||
fetch('/api/learning/ioc/beans').then(r => r.json()),
|
||||
fetch('/api/users/count').then(r => r.json()),
|
||||
fetch('/api/products').then(r => r.json()),
|
||||
fetch('/api/orders').then(r => r.json())
|
||||
fetch('/api/orders').then(r => r.json()),
|
||||
fetch('/api/profile').then(r => r.json())
|
||||
]);
|
||||
|
||||
document.getElementById('beanCount').textContent = beans.total || '-';
|
||||
document.getElementById('userCount').textContent = users.count || 0;
|
||||
document.getElementById('productCount').textContent = products.length || 0;
|
||||
document.getElementById('orderCount').textContent = orders.length || 0;
|
||||
document.getElementById('activeProfile').textContent = profile.profile || '-';
|
||||
} catch (e) {
|
||||
console.error('加载状态失败:', e);
|
||||
}
|
||||
|
||||
@@ -3,173 +3,200 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>IoC 容器学习 - Spring Boot</title>
|
||||
<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; }
|
||||
.concept-box h4 { color: #333; margin-bottom: 10px; }
|
||||
|
||||
.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; }
|
||||
|
||||
.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: 400px; overflow-y: auto; }
|
||||
|
||||
.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>控制反转 (Inversion of Control) 与依赖注入 (Dependency Injection)</p>
|
||||
<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="users.html">👥 用户</a>
|
||||
<a href="api.html">🔌 API</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>📚 核心概念</h3>
|
||||
<div class="concept-box">
|
||||
<h4>什么是 IoC?</h4>
|
||||
<p><strong>控制反转 (Inversion of Control)</strong>:将对象的创建和管理交给 Spring 容器,而不是由开发者手动创建。</p>
|
||||
<p><strong>依赖注入 (Dependency Injection)</strong>:IoC 的一种实现方式,通过构造器、Setter 或字段将依赖注入到对象中。</p>
|
||||
|
||||
<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="concept-box">
|
||||
<h4>为什么用 IoC?</h4>
|
||||
<ul>
|
||||
<li>✅ <strong>解耦</strong>:对象之间不直接依赖,通过接口交互</li>
|
||||
<li>✅ <strong>可测试</strong>:方便使用 Mock 对象进行单元测试</li>
|
||||
<li>✅ <strong>可维护</strong>:集中管理对象生命周期</li>
|
||||
<li>✅ <strong>AOP 支持</strong>:便于实现切面编程</li>
|
||||
</ul>
|
||||
|
||||
<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>
|
||||
<p>Spring 容器中管理的所有 Bean 对象</p>
|
||||
<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 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>每次请求都创建新实例</td><td>有状态的对象</td></tr>
|
||||
<tr><td><span class="badge badge-warning">request</span></td><td>每个 HTTP 请求一个实例</td><td>Web 应用</td></tr>
|
||||
<tr><td><span class="badge badge-warning">session</span></td><td>每个 HTTP 会话一个实例</td><td>用户会话数据</td></tr>
|
||||
</table>
|
||||
<button class="btn" onclick="testScopes()">测试作用域</button>
|
||||
<div id="scopesResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>⚡ 性能统计</h3>
|
||||
<p>实时监控方法执行时间和调用次数</p>
|
||||
<button class="btn" onclick="loadPerformance()">刷新统计</button>
|
||||
<button class="btn btn-secondary" onclick="resetPerformance()">重置统计</button>
|
||||
<div id="performanceResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>💉 依赖注入方式对比</h3>
|
||||
<table>
|
||||
<tr><th>方式</th><th>优点</th><th>缺点</th><th>推荐度</th></tr>
|
||||
<tr><td>构造器注入</td><td>明确依赖、不可变、易测试</td><td>参数多时代码长</td><td>⭐⭐⭐⭐⭐</td></tr>
|
||||
<tr><td>Setter 注入</td><td>可选依赖、灵活</td><td>可能为 null</td><td>⭐⭐⭐</td></tr>
|
||||
<tr><td>字段注入</td><td>代码简洁</td><td>隐藏依赖、难测试</td><td>⭐</td></tr>
|
||||
</table>
|
||||
</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\n用户相关 Bean:\n${data.userBeans.map(b => ' - ' + b).join('\n')}`;
|
||||
result.innerHTML = `<strong>总 Bean 数: ${data.total}</strong>\n\nIoC 学习相关 Bean:\n${data.userBeans.map(b => ' - ' + b).join('\n')}`;
|
||||
} catch (e) {
|
||||
result.textContent = '加载失败: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function testScopes() {
|
||||
const result = document.getElementById('scopesResult');
|
||||
try {
|
||||
const res = await fetch('/api/learning/ioc/scopes');
|
||||
const data = await res.json();
|
||||
result.innerHTML = JSON.stringify(data, null, 2);
|
||||
} catch (e) {
|
||||
result.textContent = '测试失败: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPerformance() {
|
||||
const result = document.getElementById('performanceResult');
|
||||
result.textContent = '加载中...';
|
||||
try {
|
||||
const res = await fetch('/api/learning/ioc/performance');
|
||||
const data = await res.json();
|
||||
if (Object.keys(data).length === 0) {
|
||||
result.textContent = '暂无性能数据,请先调用一些 API';
|
||||
return;
|
||||
}
|
||||
let html = '<table><tr><th>方法</th><th>调用次数</th><th>错误数</th><th>平均耗时(ms)</th><th>最大耗时(ms)</th></tr>';
|
||||
for (const [key, val] of Object.entries(data)) {
|
||||
html += `<tr><td>${key}</td><td>${val.count}</td><td>${val.errors}</td><td>${val.avgMs}</td><td>${val.maxMs}</td></tr>`;
|
||||
}
|
||||
html += '</table>';
|
||||
result.innerHTML = html;
|
||||
} catch (e) {
|
||||
result.textContent = '加载失败: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function resetPerformance() {
|
||||
try {
|
||||
await fetch('/api/learning/ioc/performance/reset', { method: 'POST' });
|
||||
loadPerformance();
|
||||
} catch (e) {
|
||||
alert('重置失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
loadBeans();
|
||||
loadPerformance();
|
||||
|
||||
loadOverview();
|
||||
loadTimeline();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
119
target/classes/static/reflection.html
Normal file
119
target/classes/static/reflection.html
Normal file
@@ -0,0 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>反射可视化实验室 - 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:1200px; margin:0 auto; padding:20px; }
|
||||
.header { background:linear-gradient(135deg,#0f766e,#14b8a6); color:#fff; padding:28px 20px; border-radius:12px; text-align:center; margin-bottom:20px; }
|
||||
.nav { display:flex; gap:10px; flex-wrap:wrap; justify-content:center; margin-bottom:20px; }
|
||||
.nav a { padding:10px 18px; background:#fff; border-radius:20px; text-decoration:none; color:#333; }
|
||||
.nav a.active { background:#0f766e; color:#fff; }
|
||||
.lab { background:#fff7e6; border-left:4px solid #fa8c16; padding:15px; border-radius:8px; margin-bottom:20px; }
|
||||
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(320px,1fr)); gap:16px; }
|
||||
.card { background:#fff; border-radius:12px; padding:18px; box-shadow:0 2px 10px rgba(0,0,0,.08); }
|
||||
.card h3 { color:#0f766e; margin-bottom:10px; }
|
||||
.result { background:#111827; color:#d1d5db; padding:12px; border-radius:8px; min-height:180px; white-space:pre-wrap; font-family:monospace; margin-top:10px; overflow:auto; }
|
||||
button { padding:10px 14px; background:#0f766e; color:#fff; border:none; border-radius:6px; cursor:pointer; margin-right:8px; margin-bottom:8px; }
|
||||
input { padding:10px; border:1px solid #ddd; border-radius:6px; margin-right:8px; margin-bottom:8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🪞 反射可视化实验室</h1>
|
||||
<p>不只说“反射很重要”,而是让你自己查看类信息、动态构造对象、读写字段、调用私有方法。</p>
|
||||
</div>
|
||||
|
||||
<div class="nav">
|
||||
<a href="index.html">🏠 首页</a>
|
||||
<a href="ioc.html">📦 IoC</a>
|
||||
<a href="reflection.html" class="active">🪞 反射</a>
|
||||
<a href="verify-lab.html">🩺 修复验证</a>
|
||||
</div>
|
||||
|
||||
<div class="lab">
|
||||
<strong>实验任务卡</strong>
|
||||
<ul style="padding-left:20px; line-height:1.8; margin-top:8px;">
|
||||
<li>先看类信息,理解反射能拿到哪些元数据</li>
|
||||
<li>再用构造器动态创建对象,观察实例内容</li>
|
||||
<li>再读写私有字段,理解为什么框架可以“跳过表面上的访问限制”</li>
|
||||
<li>最后调用私有方法和静态方法,感受反射为什么是框架底层利器</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>概念总览</h3>
|
||||
<button onclick="callApi('/api/learning/reflection/overview','overview')">加载总览</button>
|
||||
<div id="overview" class="result"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>类元信息</h3>
|
||||
<button onclick="callApi('/api/learning/reflection/class-info','classInfo')">查看类信息</button>
|
||||
<div id="classInfo" class="result"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>动态构造对象</h3>
|
||||
<input id="ctorName" value="ref-user" placeholder="name">
|
||||
<input id="ctorCount" value="5" placeholder="count">
|
||||
<button onclick="instantiate()">反射创建对象</button>
|
||||
<div id="ctorResult" class="result"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>私有字段读写</h3>
|
||||
<input id="fieldValue" value="changed-by-reflection" placeholder="new field value">
|
||||
<button onclick="fieldAccess()">修改私有字段</button>
|
||||
<div id="fieldResult" class="result"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>方法调用实验</h3>
|
||||
<input id="prefix" value="你好" placeholder="prefix">
|
||||
<button onclick="methodCall()">调用公开/私有/静态方法</button>
|
||||
<div id="methodResult" class="result"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>框架为什么依赖反射</h3>
|
||||
<button onclick="callApi('/api/learning/reflection/why-frameworks-use-it','frameworkResult')">查看框架视角</button>
|
||||
<div id="frameworkResult" class="result"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
async function callApi(url, id) {
|
||||
const el = document.getElementById(id);
|
||||
el.textContent = '加载中...';
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
el.textContent = JSON.stringify(data, null, 2);
|
||||
} catch (e) {
|
||||
el.textContent = '失败: ' + e.message;
|
||||
}
|
||||
}
|
||||
async function instantiate() {
|
||||
const name = encodeURIComponent(document.getElementById('ctorName').value);
|
||||
const count = encodeURIComponent(document.getElementById('ctorCount').value);
|
||||
callApi(`/api/learning/reflection/instantiate?name=${name}&count=${count}`, 'ctorResult');
|
||||
}
|
||||
async function fieldAccess() {
|
||||
const value = encodeURIComponent(document.getElementById('fieldValue').value);
|
||||
callApi(`/api/learning/reflection/field-access?value=${value}`, 'fieldResult');
|
||||
}
|
||||
async function methodCall() {
|
||||
const prefix = encodeURIComponent(document.getElementById('prefix').value);
|
||||
callApi(`/api/learning/reflection/method-call?prefix=${prefix}`, 'methodResult');
|
||||
}
|
||||
callApi('/api/learning/reflection/overview','overview');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
120
target/classes/static/verify-lab.html
Normal file
120
target/classes/static/verify-lab.html
Normal file
@@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>修复验证实验室 - 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:1200px; margin:0 auto; padding:20px; }
|
||||
.header { background:linear-gradient(135deg,#667eea,#764ba2); color:#fff; padding:28px 20px; border-radius:12px; text-align:center; margin-bottom:20px; }
|
||||
.nav { display:flex; gap:10px; flex-wrap:wrap; justify-content:center; margin-bottom:20px; }
|
||||
.nav a { padding:10px 18px; background:#fff; border-radius:20px; text-decoration:none; color:#333; }
|
||||
.nav a.active { background:#667eea; color:#fff; }
|
||||
.lab { background:#fff7e6; border-left:4px solid #fa8c16; padding:16px; border-radius:8px; margin-bottom:20px; }
|
||||
.grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap:16px; }
|
||||
.card { background:#fff; border-radius:12px; padding:18px; box-shadow:0 2px 10px rgba(0,0,0,.08); }
|
||||
.card h3 { margin-bottom:12px; color:#333; }
|
||||
.result { background:#111827; color:#d1d5db; padding:12px; border-radius:8px; min-height:160px; white-space:pre-wrap; font-family:monospace; margin-top:10px; }
|
||||
button { padding:10px 14px; background:#667eea; color:#fff; border:none; border-radius:6px; cursor:pointer; margin-right:8px; margin-bottom:8px; }
|
||||
.status-pass { color:#16a34a; font-weight:700; }
|
||||
.status-fail { color:#dc2626; font-weight:700; }
|
||||
.hint { color:#666; line-height:1.7; margin-top:8px; }
|
||||
.summary { background:#fff; border-radius:12px; padding:18px; margin-bottom:20px; box-shadow:0 2px 10px rgba(0,0,0,.08); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🩺 修复验证实验室</h1>
|
||||
<p>不是“我说修好了”,而是你自己点检查,看到哪里坏、为什么坏、修没修好。</p>
|
||||
</div>
|
||||
|
||||
<div class="nav">
|
||||
<a href="index.html">🏠 首页</a>
|
||||
<a href="auth-lab.html">🔐 鉴权实验室</a>
|
||||
<a href="api.html">🔌 API 测试</a>
|
||||
<a href="verify-lab.html" class="active">🩺 修复验证实验室</a>
|
||||
</div>
|
||||
|
||||
<div class="lab">
|
||||
<strong>参与式验证任务卡</strong>
|
||||
<ul style="padding-left:20px;line-height:1.8;margin-top:8px;">
|
||||
<li>先点“一键跑总览”,确认当前 profile / auth / datasource 状态</li>
|
||||
<li>再分别点数据库、用户服务、MyBatis 产品查询,感受“哪个环节坏了就在哪一步暴露”</li>
|
||||
<li>最后打开 H2 Console,验证修复后跳转是不是仍保持 https</li>
|
||||
<li>如果某一步失败,不要只看失败,要看返回里的 hint —— 那是排查方向</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="summary">
|
||||
<button onclick="runOverview()">一键跑总览</button>
|
||||
<button onclick="openH2()">打开 H2 Console</button>
|
||||
<div id="summaryResult" class="result">点击按钮开始验证...</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>数据库链路</h3>
|
||||
<button onclick="runCheck('database','dbResult')">检查数据库</button>
|
||||
<div class="hint">验证 DataSource / JDBC / H2 是否可用。</div>
|
||||
<div id="dbResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>用户服务链路</h3>
|
||||
<button onclick="runCheck('users','userResult')">检查用户服务</button>
|
||||
<div class="hint">验证 JPA / Service / 用户数据查询是否正常。</div>
|
||||
<div id="userResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>MyBatis 链路</h3>
|
||||
<button onclick="runCheck('products','productResult')">检查产品查询</button>
|
||||
<div class="hint">验证 MyBatis Mapper / SQL / 数据库查询是否正常。</div>
|
||||
<div id="productResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>鉴权模式</h3>
|
||||
<button onclick="runCheck('auth','authResult')">检查鉴权模式</button>
|
||||
<div class="hint">对照当前 profile,理解 why learn=none / advanced=jwt。</div>
|
||||
<div id="authResult" class="result"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
async function runOverview() {
|
||||
const el = document.getElementById('summaryResult');
|
||||
el.textContent = '检查中...';
|
||||
try {
|
||||
const res = await fetch('/api/verify/overview');
|
||||
const data = await res.json();
|
||||
el.textContent = JSON.stringify(data, null, 2);
|
||||
} catch (e) {
|
||||
el.textContent = '总览检查失败: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function runCheck(name, resultId) {
|
||||
const el = document.getElementById(resultId);
|
||||
el.textContent = '检查中...';
|
||||
try {
|
||||
const res = await fetch('/api/verify/' + name);
|
||||
const data = await res.json();
|
||||
const badge = data.ok ? '✅ PASS' : '❌ FAIL';
|
||||
el.textContent = badge + '\n\n' + JSON.stringify(data, null, 2);
|
||||
} catch (e) {
|
||||
el.textContent = '检查失败: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
function openH2() {
|
||||
window.open('/h2-console', '_blank');
|
||||
}
|
||||
|
||||
runOverview();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user