Update: cache module, security modules and build
This commit is contained in:
46
target/classes/application-advanced.yml
Normal file
46
target/classes/application-advanced.yml
Normal file
@@ -0,0 +1,46 @@
|
||||
# 高级配置 - 可插拔组件
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: advanced
|
||||
|
||||
# 数据库选择: h2 / mysql / postgresql
|
||||
datasource:
|
||||
driver-class-name: ${DB_DRIVER:org.h2.Driver}
|
||||
url: ${DB_URL:jdbc:h2:file:~/h2/springboot_scaffold}
|
||||
username: ${DB_USER:sa}
|
||||
password: ${DB_PASS:}
|
||||
|
||||
# Redis 缓存
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASS:}
|
||||
database: ${REDIS_DB:0}
|
||||
timeout: 3000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
# 鉴权方案选择: none / jwt / satoken
|
||||
auth:
|
||||
type: ${AUTH_TYPE:jwt} # none | jwt | satoken
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:your-secret-key}
|
||||
expiration: ${JWT_EXPIRATION:86400000} # 24小时
|
||||
satoken:
|
||||
timeout: ${SA_TIMEOUT:86400} # 24小时
|
||||
activity-timeout: ${SA_ACTIVITY_TIMEOUT:1800} # 30分钟
|
||||
|
||||
# 缓存配置
|
||||
cache:
|
||||
type: ${CACHE_TYPE:caffeine} # caffeine | redis
|
||||
redis:
|
||||
time-to-live: 600000 # 10分钟
|
||||
|
||||
# MyBatis 多数据库适配
|
||||
mybatis:
|
||||
configuration:
|
||||
database-id: ${DB_TYPE:h2} # h2 | mysql | postgresql
|
||||
29
target/classes/application-learn.yml
Normal file
29
target/classes/application-learn.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
# Learn profile: keep dependencies and runtime simple for first-round learning
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: learn
|
||||
datasource:
|
||||
url: jdbc:h2:mem:springboot_scaffold_learn;MODE=MYSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
|
||||
app:
|
||||
profile: learn
|
||||
enabled-modules:
|
||||
- ioc
|
||||
- aop
|
||||
- mybatis
|
||||
- transaction
|
||||
- users
|
||||
|
||||
auth:
|
||||
type: none
|
||||
learning-note: "learn 模式默认不开启鉴权,专注 Spring 核心学习"
|
||||
|
||||
cache:
|
||||
type: caffeine
|
||||
@@ -1,7 +1,10 @@
|
||||
spring.application.name=springboot-scaffold
|
||||
server.port=8082
|
||||
server.port=8083
|
||||
server.forward-headers-strategy=framework
|
||||
|
||||
# H2 Database
|
||||
spring.profiles.active=${APP_PROFILE:learn}
|
||||
|
||||
# H2 Database (default baseline; learn profile overrides to in-memory)
|
||||
spring.h2.console.enabled=true
|
||||
spring.datasource.url=jdbc:h2:file:~/h2/springboot_scaffold
|
||||
spring.datasource.driverClassName=org.h2.Driver
|
||||
|
||||
Binary file not shown.
BIN
target/classes/com/example/scaffold/cache/CacheConfig.class
vendored
Normal file
BIN
target/classes/com/example/scaffold/cache/CacheConfig.class
vendored
Normal file
Binary file not shown.
BIN
target/classes/com/example/scaffold/cache/CacheService.class
vendored
Normal file
BIN
target/classes/com/example/scaffold/cache/CacheService.class
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/example/scaffold/security/jwt/JwtUtil.class
Normal file
BIN
target/classes/com/example/scaffold/security/jwt/JwtUtil.class
Normal file
Binary file not shown.
Binary file not shown.
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