Files
springboot-demo/target/classes/static/events.html

253 lines
10 KiB
HTML
Raw Normal View History

<!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; max-width: 900px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
h1 { color: #6DB33F; margin: 20px 0; }
h2 { color: #333; border-bottom: 2px solid #6DB33F; padding-bottom: 10px; margin: 20px 0 15px; }
h3 { color: #6DB33F; margin: 15px 0 10px; }
.card { background: white; padding: 25px; margin: 15px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.btn { display: inline-block; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 500; cursor: pointer; border: none; margin: 5px; }
.btn-primary { background: #6DB33F; color: white; }
.btn-primary:hover { background: #5da32f; }
.btn-info { background: #17a2b8; color: white; }
.btn-warning { background: #ffc107; color: #333; }
code { background: #f0f0f0; padding: 2px 8px; border-radius: 4px; font-family: 'Fira Code', monospace; }
pre { background: #2d2d2d; color: #f8f8f2; padding: 20px; border-radius: 8px; overflow-x: auto; margin: 15px 0; }
pre code { background: none; color: inherit; }
.tip { background: #e7f3ff; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #6DB33F; }
.result-box { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 8px; font-family: monospace; white-space: pre-wrap; margin-top: 10px; max-height: 300px; overflow-y: auto; }
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
th { background: #6DB33F; color: white; }
.event-flow { display: flex; align-items: center; justify-content: center; margin: 20px 0; }
.event-flow span { padding: 15px 25px; background: #6DB33F; color: white; border-radius: 8px; margin: 0 10px; }
.event-flow .arrow { font-size: 24px; color: #6DB33F; }
.nav { margin-bottom: 20px; }
.nav a { margin-right: 15px; color: #6DB33F; text-decoration: none; }
.nav a:hover { text-decoration: underline; }
2026-03-18 15:18:30 +08:00
.lab { background:#fff7e6; border-left:4px solid #fa8c16; padding:15px; border-radius:8px; margin:15px 0; }
.lab h4 { color:#ad6800; margin-bottom:8px; }
</style>
</head>
<body>
<div class="nav">
<a href="/">← 返回首页</a>
<a href="/users.html">用户管理</a>
<a href="/aop.html">AOP 切面</a>
</div>
<h1>📡 Spring 事件机制</h1>
2026-03-18 15:18:30 +08:00
<div class="lab">
<h4>🧪 实验任务卡(事件)</h4>
<label style="display:block;margin-bottom:8px;"><input id="eventTaskDone" type="checkbox" onchange="toggleEventTaskDone(this)"> 本任务我已经完成</label>
<ul style="padding-left:20px;line-height:1.8;">
<li>目标:体验发布者与监听者解耦</li>
<li>步骤1输入 userId/userName点击“发布登录事件”</li>
<li>步骤2重复发布不同用户比较返回结果</li>
<li>预期:接口立即返回;监听处理在日志中可观察</li>
<li>常见坑:把事件当同步 RPC忽略异步监听特性</li>
</ul>
</div>
<div class="card">
<h3>🎉 事件发布演示</h3>
<p>模拟用户登录事件,观察事件发布和监听过程</p>
<div style="margin: 15px 0;">
<input type="text" id="userName" placeholder="用户名" value="张三" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; width: 150px;">
<input type="number" id="userId" placeholder="用户ID" value="1" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; width: 100px;">
<button class="btn btn-primary" onclick="publishEvent()">发布登录事件</button>
2026-03-18 15:18:30 +08:00
<button class="btn btn-warning" onclick="demoEventError()">演示参数错误</button>
</div>
<div class="result-box" id="eventResult">等待事件发布...</div>
</div>
<h2>🔄 事件机制流程</h2>
<div class="event-flow">
<span>发布者</span>
<span class="arrow"></span>
<span>ApplicationEventPublisher</span>
<span class="arrow"></span>
<span>事件</span>
<span class="arrow"></span>
<span>监听者</span>
</div>
<div class="tip">
<strong>核心优势:</strong>
<ul style="margin-top: 10px; padding-left: 20px;">
<li><strong>解耦:</strong>发布者和监听者互不依赖</li>
<li><strong>扩展:</strong>新增监听器无需修改发布者</li>
<li><strong>异步:</strong>耗时操作不阻塞主流程</li>
</ul>
</div>
<h2>💻 代码实现</h2>
<div class="card">
<h3>1. 定义事件</h3>
<pre><code>public class UserEvent {
public enum Type { CREATED, UPDATED, DELETED, LOGIN }
private Type type;
private Long userId;
private String userName;
private LocalDateTime timestamp;
// constructor, getters...
}</code></pre>
</div>
<div class="card">
<h3>2. 发布事件</h3>
<pre><code>@Component
public class UserEventPublisher {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void publishUserLogin(Long userId, String userName) {
UserEvent event = new UserEvent(
UserEvent.Type.LOGIN,
userId,
userName,
"用户登录成功"
);
eventPublisher.publishEvent(event);
}
}</code></pre>
</div>
<div class="card">
<h3>3. 监听事件</h3>
<pre><code>@Component
public class UserEventListener {
// 基础监听
@EventListener
public void handleUserEvent(UserEvent event) {
System.out.println("收到事件: " + event.getType());
}
// 条件监听 - 只处理登录事件
@EventListener(condition = "#event.type == T(com.example.demo.model.UserEvent$Type).LOGIN")
public void handleLogin(UserEvent event) {
System.out.println("用户登录: " + event.getUserName());
}
// 异步监听 - 不阻塞主流程
@Async
@EventListener
public void sendWelcomeEmail(UserEvent event) {
// 发送邮件...
}
}</code></pre>
</div>
<div class="card">
<h3>4. 控制器中使用</h3>
<pre><code>@RestController
@RequestMapping("/aop")
public class AopEventController {
@Autowired
private UserEventPublisher eventPublisher;
@PostMapping("/event/publish")
public Map&lt;String, Object&gt; publishEvent(
@RequestParam Long userId,
@RequestParam String userName) {
// 发布事件
eventPublisher.publishUserLogin(userId, userName);
return Map.of(
"message", "事件已发布",
"userId", userId,
"userName", userName
);
}
}</code></pre>
</div>
<h2>🎯 应用场景</h2>
<div class="card">
<table>
<tr><th>场景</th><th>事件类型</th><th>处理逻辑</th></tr>
<tr><td>用户注册</td><td>UserCreatedEvent</td><td>发送欢迎邮件、初始化数据</td></tr>
<tr><td>订单创建</td><td>OrderCreatedEvent</td><td>扣库存、发送通知</td></tr>
<tr><td>支付成功</td><td>PaymentSuccessEvent</td><td>更新订单状态、发送短信</td></tr>
<tr><td>用户登录</td><td>UserLoginEvent</td><td>记录登录日志、更新在线状态</td></tr>
</table>
</div>
<div class="card">
<h3>💡 最佳实践</h3>
<ul style="line-height: 2; padding-left: 20px;">
<li>事件类应该是<strong>不可变</strong>的(只读属性)</li>
<li>使用 <code>@Async</code> 处理耗时操作</li>
<li>避免在监听器中抛出异常</li>
<li>使用 <code>condition</code> 过滤不需要的事件</li>
<li>复杂场景考虑使用消息队列RabbitMQ/Kafka</li>
</ul>
</div>
<p style="margin-top: 30px;"><a href="/">← 返回学习中心</a></p>
<script>
2026-03-18 15:18:30 +08:00
const EVENT_TASK_KEY = 'task.event.done';
function toggleEventTaskDone(el) {
localStorage.setItem(EVENT_TASK_KEY, el.checked ? '1' : '0');
}
function initEventTaskState() {
const done = localStorage.getItem(EVENT_TASK_KEY) === '1';
const checkbox = document.getElementById('eventTaskDone');
if (checkbox) checkbox.checked = done;
}
async function demoEventError() {
const resultBox = document.getElementById('eventResult');
try {
const res = await fetch('/aop/event/publish?userName=', { method: 'POST' });
const data = await res.json();
resultBox.textContent = JSON.stringify(data, null, 2);
} catch (e) {
resultBox.textContent = '错误: ' + e.message;
}
}
async function publishEvent() {
const userId = document.getElementById('userId').value;
const userName = document.getElementById('userName').value;
try {
const res = await fetch(`/aop/event/publish?userId=${userId}&userName=${encodeURIComponent(userName)}`, {
method: 'POST'
});
const data = await res.json();
const resultBox = document.getElementById('eventResult');
resultBox.textContent = `[${new Date().toLocaleTimeString()}] 事件已发布\n\n` +
JSON.stringify(data, null, 2) + '\n\n' +
'📊 查看控制台日志可以看到监听器的输出:\n' +
'[EventPublisher] 发布事件: LOGIN - ' + userName + '\n' +
'[EventListener] 收到事件: LOGIN - 用户: ' + userName + '\n' +
'[LoginTracker] 记录用户登录: ' + userName;
} catch (e) {
document.getElementById('eventResult').textContent = '错误: ' + e.message;
}
}
2026-03-18 15:18:30 +08:00
initEventTaskState();
</script>
</body>
</html>