253 lines
10 KiB
HTML
253 lines
10 KiB
HTML
<!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; }
|
||
.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>
|
||
|
||
<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>
|
||
<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<String, Object> 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>
|
||
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;
|
||
}
|
||
}
|
||
|
||
initEventTaskState();
|
||
</script>
|
||
</body>
|
||
</html> |