feat: add guided learning cockpit

This commit is contained in:
Codex
2026-03-19 13:53:49 +08:00
parent 00306082fb
commit 09574c3400
8 changed files with 967 additions and 800 deletions

View File

@@ -9,61 +9,35 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 性能监控切面 - 演示 @Around 环绕通知
*
* 学习点:
* - @Around 最强大的通知类型
* - ProceedingJoinPoint 控制方法执行
* - 方法执行时间统计
*/
@Aspect
@Component
public class PerformanceAspect {
// 方法执行时间统计
private final Map<String, Long> totalTimes = new ConcurrentHashMap<>();
private final Map<String, Long> callCounts = new ConcurrentHashMap<>();
/**
* 环绕通知 - 统计方法执行时间
*/
@Around("execution(* com.example.demo.controller.*.*(..)) || " +
"execution(* com.example.demo.service.*.*(..))")
"execution(* com.example.demo.service.*.*(..))")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().toShortString();
long startTime = System.currentTimeMillis();
try {
// 执行目标方法
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// 记录统计
long duration = System.currentTimeMillis() - startTime;
totalTimes.merge(methodName, duration, Long::sum);
callCounts.merge(methodName, 1L, Long::sum);
System.out.println("[AOP-Performance] " + methodName +
" 执行耗时: " + duration + "ms");
System.out.println("[AOP-Performance] " + methodName + " completed in " + duration + "ms");
return result;
} catch (Throwable e) {
long endTime = System.currentTimeMillis();
System.out.println("[AOP-Performance] " + methodName +
" 异常耗时: " + (endTime - startTime) + "ms");
throw e;
} catch (Throwable error) {
long duration = System.currentTimeMillis() - startTime;
System.out.println("[AOP-Performance] " + methodName + " failed after " + duration + "ms");
throw error;
}
}
/**
* 获取性能统计
*/
public Map<String, Map<String, Long>> getStatistics() {
Map<String, Map<String, Long>> stats = new HashMap<>();
for (String method : totalTimes.keySet()) {
Map<String, Long> methodStats = new HashMap<>();
methodStats.put("totalTime", totalTimes.get(method));
@@ -71,7 +45,6 @@ public class PerformanceAspect {
methodStats.put("avgTime", totalTimes.get(method) / callCounts.get(method));
stats.put(method, methodStats);
}
return stats;
}
}
}

View File

@@ -3,177 +3,178 @@ package com.example.demo.controller;
import com.example.demo.aop.PerformanceAspect;
import com.example.demo.aop.RateLimitAspect;
import com.example.demo.aop.RateLimited;
import com.example.demo.event.UserEventListener;
import com.example.demo.event.UserEventPublisher;
import com.example.demo.model.User;
import com.example.demo.model.UserEvent;
import com.example.demo.service.UserService;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* AOP 和事件机制学习控制器
*/
@RestController
@RequestMapping("/aop")
public class AopEventController {
private final UserService userService;
private final UserEventPublisher eventPublisher;
private final UserEventListener userEventListener;
private final PerformanceAspect performanceAspect;
private final RateLimitAspect rateLimitAspect;
public AopEventController(UserService userService,
UserEventPublisher eventPublisher,
UserEventListener userEventListener,
PerformanceAspect performanceAspect,
RateLimitAspect rateLimitAspect) {
this.userService = userService;
this.eventPublisher = eventPublisher;
this.userEventListener = userEventListener;
this.performanceAspect = performanceAspect;
this.rateLimitAspect = rateLimitAspect;
}
/**
* 学习首页
*/
@GetMapping
public Map<String, Object> index() {
Map<String, Object> info = new HashMap<>();
info.put("message", "Spring Boot 学习中心");
info.put("message", "Explore cross-cutting behavior through AOP, events, and rate limiting.");
info.put("topics", new String[]{
"AOP 切面编程",
"事件机制",
"Bean 生命周期"
"AOP advice and performance tracing",
"Application events and listener decoupling",
"Annotation-driven rate limiting"
});
info.put("endpoints", new String[]{
"GET /aop - AOP 概念说明",
"GET /aop/stats - 性能统计",
"GET /aop/event - 事件机制说明",
"POST /aop/event/publish - 发布用户事件",
"GET /aop/event/history - 查看事件历史",
"GET /aop/ratelimit - 限流测试"
"GET /aop/aop",
"GET /aop/aop/stats",
"GET /aop/event",
"POST /aop/event/publish",
"GET /aop/event/history",
"GET /aop/ratelimit"
});
info.put("userCount", userService.findAll().size());
return info;
}
// ==================== AOP 示例 ====================
/**
* AOP 概念说明
*/
@GetMapping("/aop")
public Map<String, Object> aopInfo() {
Map<String, Object> info = new HashMap<>();
info.put("title", "AOP 切面编程");
info.put("title", "AOP learning notes");
info.put("concepts", new String[]{
"Aspect(切面): 横切关注点的模块化",
"JoinPoint(连接点): 程序执行的某个点",
"Pointcut(切入点): 匹配连接点的表达式",
"Advice(通知): 在连接点执行的动作",
"Weaving(织入): 将切面应用到目标对象"
"Aspect: a reusable cross-cutting module",
"Join point: a place where the program can be intercepted",
"Pointcut: the matcher that selects join points",
"Advice: code that runs before, after, or around the join point",
"Weaving: combining the aspect with the target object"
});
info.put("adviceTypes", new String[]{
"@Before - 方法执行前",
"@After - 方法执行后(无论成功或异常)",
"@AfterReturning - 方法成功返回后",
"@AfterThrowing - 方法抛出异常后",
"@Around - 环绕通知(最强大)"
"@Before for pre-checks",
"@After for cleanup",
"@AfterReturning for successful results",
"@AfterThrowing for failures",
"@Around for timing, wrapping, and total control"
});
info.put("useCases", new String[]{
"日志记录",
"性能监控",
"事务管理",
"权限检查",
"限流控制"
"Logging",
"Performance measurement",
"Authorization checks",
"Rate limiting",
"Reusable validation"
});
return info;
}
/**
* 查看性能统计
*/
@GetMapping("/aop/stats")
public Map<String, Object> getPerformanceStats() {
Map<String, Object> result = new HashMap<>();
result.put("title", "方法性能统计");
result.put("description", "由 PerformanceAspect 自动收集");
result.put("title", "Collected performance metrics");
result.put("description", "These numbers come from the around advice on controllers and services.");
result.put("statistics", performanceAspect.getStatistics());
return result;
}
// ==================== 事件机制示例 ====================
/**
* 事件机制说明
*/
@GetMapping("/event")
public Map<String, Object> eventInfo() {
Map<String, Object> info = new HashMap<>();
info.put("title", "Spring 事件机制");
info.put("title", "Spring application events");
info.put("concepts", new String[]{
"ApplicationEventPublisher - 事件发布者",
"@EventListener - 事件监听器",
"@Async - 异步处理事件",
"condition - 条件过滤"
"ApplicationEventPublisher emits a domain or lifecycle event.",
"@EventListener reacts without tight coupling to the caller.",
"@Async lets slow follow-up work happen outside the request path.",
"Conditions help one listener focus on one event type."
});
info.put("benefits", new String[]{
"解耦:发布者和监听者互不依赖",
"扩展:新增监听器无需修改发布者",
"异步:耗时操作不阻塞主流程",
"测试:更容易进行单元测试"
});
info.put("eventTypes", new String[]{
"CREATED - 用户创建",
"UPDATED - 用户更新",
"DELETED - 用户删除",
"LOGIN - 用户登录"
"Controllers stay smaller.",
"Listeners can grow independently.",
"The request flow stays responsive.",
"Side effects become easier to test."
});
info.put("eventTypes", UserEvent.Type.values());
return info;
}
/**
* 发布用户事件
*/
@PostMapping("/event/publish")
public Map<String, Object> publishEvent(
@RequestParam Long userId,
@RequestParam String userName,
@RequestParam(defaultValue = "LOGIN") String eventType
@RequestParam Long userId,
@RequestParam String userName,
@RequestParam(defaultValue = "LOGIN") String eventType
) {
eventPublisher.publishUserLogin(userId, userName);
UserEvent.Type type = resolveType(eventType);
String detail = switch (type) {
case CREATED -> "User was created through the demo flow";
case UPDATED -> "User profile was updated through the demo flow";
case DELETED -> "User was removed through the demo flow";
case LOGIN -> "User signed in through the demo flow";
};
eventPublisher.publishUserEvent(userId, userName, type, detail);
Map<String, Object> result = new HashMap<>();
result.put("message", "事件已发布");
result.put("eventType", eventType);
result.put("message", "Event published successfully.");
result.put("eventType", type.name());
result.put("userId", userId);
result.put("userName", userName);
result.put("historySize", userEventListener.getEventHistory().size());
return result;
}
/**
* 查看事件历史
*/
@GetMapping("/event/history")
public Map<String, Object> getEventHistory() {
List<Map<String, Object>> items = userEventListener.getEventHistory().stream()
.map(event -> Map.<String, Object>of(
"type", event.getType().name(),
"userId", event.getUserId(),
"userName", event.getUserName(),
"detail", event.getDetail(),
"timestamp", event.getTimestamp().toString()
))
.toList();
Map<String, Object> result = new HashMap<>();
result.put("title", "事件历史记录");
result.put("note", "由 UserEventListener 自动记录");
result.put("title", "Recent user event history");
result.put("total", items.size());
result.put("items", items);
return result;
}
// ==================== 限流示例 ====================
/**
* 限流测试接口
*/
@GetMapping("/ratelimit")
@RateLimited(value = 10, message = "测试限流每分钟最多10次")
@RateLimited(value = 10, message = "Demo limit reached: only 10 requests per minute are allowed.")
public Map<String, Object> testRateLimit() {
Map<String, Object> result = new HashMap<>();
result.put("message", "请求成功");
result.put("note", "使用 @RateLimited 注解");
result.put("message", "Rate-limited endpoint executed successfully.");
result.put("note", "The @RateLimited annotation wraps this method through an aspect.");
result.put("rateLimitStatus", rateLimitAspect.getRateLimitStatus());
return result;
}
}
private UserEvent.Type resolveType(String rawType) {
try {
return UserEvent.Type.valueOf(rawType.trim().toUpperCase());
} catch (Exception ignored) {
return UserEvent.Type.LOGIN;
}
}
}

View File

@@ -1,118 +1,120 @@
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 学习示例控制器
*
* 学习点:
* - 各种参数接收方式
* - 配置注入
* - 响应格式
*/
@RestController
public class LearnController {
// 从配置文件注入值
@Value("${spring.application.name:demo}")
private String appName;
/**
* 根路径 - 重定向到学习中心
*/
@GetMapping("/")
@GetMapping("/learn/root")
public Map<String, Object> root() {
Map<String, Object> info = new HashMap<>();
info.put("message", "欢迎来到 Spring Boot 学习脚手架!");
info.put("learn", "https://spring.xiaoxiaoluohao.indevs.in/learn");
info.put("aop", "https://spring.xiaoxiaoluohao.indevs.in/aop");
info.put("api", "https://spring.xiaoxiaoluohao.indevs.in/api/users");
info.put("message", "Welcome to the Spring Boot learning workspace.");
info.put("homePage", "/");
info.put("userLab", "/users.html");
info.put("aopLab", "/aop.html");
info.put("eventLab", "/events.html");
info.put("learnApi", "/learn");
return info;
}
// GET /learn - API 信息
@GetMapping("/learn")
public Map<String, Object> info() {
Map<String, Object> info = new HashMap<>();
info.put("app", appName);
info.put("message", "欢迎学习 Spring Boot");
info.put("message", "Use this endpoint family to compare common Spring MVC input patterns.");
info.put("learningGoals", new String[]{
"Understand how query parameters, path variables, headers, cookies, and JSON bodies are mapped.",
"Compare normal responses with handled exceptions.",
"Connect the browser, controller method signature, and serialized JSON output."
});
info.put("endpoints", new String[]{
"GET /learn/params?name=xxx&age=18 - 参数示例",
"POST /learn/body - JSON 请求体示例",
"GET /learn/path/{id} - 路径变量示例",
"GET /learn/header - 请求头示例",
"GET /learn/cookie - Cookie 示例",
"POST /api/auth/login - 学习用 JWT 登录",
"GET /api/secure/me - 受保护接口(需 Bearer Token"
"GET /learn/params?name=alex&age=18",
"POST /learn/body",
"GET /learn/path/{id}",
"GET /learn/header",
"GET /learn/cookie",
"GET /learn/exception",
"POST /api/auth/login",
"GET /api/secure/me"
});
return info;
}
// GET /learn/params?name=xxx&age=18 - 查询参数
@GetMapping("/learn/params")
public Map<String, Object> params(
@RequestParam(required = false, defaultValue = "游客") String name,
@RequestParam(required = false, defaultValue = "0") Integer age
@RequestParam(required = false, defaultValue = "guest") String name,
@RequestParam(required = false, defaultValue = "0") Integer age
) {
Map<String, Object> result = new HashMap<>();
result.put("pattern", "@RequestParam");
result.put("name", name);
result.put("age", age);
result.put("tip", "使用 @RequestParam 接收查询参数");
result.put("tip", "Query parameters are useful for filters, pagination, and optional toggles.");
return result;
}
// POST /learn/body - 请求体
@PostMapping("/learn/body")
public Map<String, Object> body(@RequestBody Map<String, Object> data) {
Map<String, Object> result = new HashMap<>();
result.put("pattern", "@RequestBody");
result.put("received", data);
result.put("tip", "使用 @RequestBody 接收 JSON 请求体");
result.put("tip", "JSON request bodies fit create and update flows where the payload has structure.");
return result;
}
// GET /learn/path/{id} - 路径变量
@GetMapping("/learn/path/{id}")
public Map<String, Object> path(@PathVariable String id) {
Map<String, Object> result = new HashMap<>();
result.put("pattern", "@PathVariable");
result.put("id", id);
result.put("tip", "使用 @PathVariable 接收路径变量");
result.put("tip", "Path variables usually identify a concrete resource such as a user, order, or lesson.");
return result;
}
// GET /learn/header - 请求头
@GetMapping("/learn/header")
public Map<String, Object> header(@RequestHeader(value = "User-Agent", required = false) String userAgent) {
Map<String, Object> result = new HashMap<>();
result.put("pattern", "@RequestHeader");
result.put("userAgent", userAgent);
result.put("tip", "使用 @RequestHeader 获取请求头");
result.put("tip", "Headers often carry auth context, tracing ids, and client metadata.");
return result;
}
// GET /learn/cookie - Cookie
@GetMapping("/learn/cookie")
public Map<String, Object> cookie(@CookieValue(value = "JSESSIONID", required = false) String sessionId) {
Map<String, Object> result = new HashMap<>();
result.put("pattern", "@CookieValue");
result.put("sessionId", sessionId);
result.put("tip", "使用 @CookieValue 获取 Cookie");
result.put("tip", "Cookies help explain browser session state and why a request may look authenticated.");
return result;
}
// GET /learn/exception - 异常处理
@GetMapping("/learn/exception")
public String exception() {
throw new RuntimeException("这是一个测试异常");
throw new RuntimeException("Intentional demo exception from /learn/exception");
}
// 全局异常处理
@ExceptionHandler(RuntimeException.class)
public Map<String, Object> handleException(RuntimeException e) {
Map<String, Object> result = new HashMap<>();
result.put("pattern", "@ExceptionHandler");
result.put("error", e.getMessage());
result.put("tip", "使用 @ExceptionHandler 处理异常");
result.put("tip", "Spring can convert thrown exceptions into structured API responses without leaking stack traces.");
return result;
}
}
}

View File

@@ -1,80 +1,50 @@
package com.example.demo.event;
import com.example.demo.model.UserEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 事件监听器 - 演示如何监听事件
*
* 学习点:
* - @EventListener 注解
* - @Async 异步处理
* - 多监听器协作
* - 事件驱动架构的优势
*/
@Component
public class UserEventListener {
// 存储事件历史(演示用)
private final List<UserEvent> eventHistory = new CopyOnWriteArrayList<>();
/**
* 监听用户事件 - 日志记录
*/
@EventListener
public void handleUserEvent(UserEvent event) {
System.out.println("[EventListener] 收到事件: " + event.getType() +
" - 用户: " + event.getUserName() +
" - 时间: " + event.getTimestamp());
// 记录到历史
System.out.println("[EventListener] Received " + event.getType() + " for " + event.getUserName()
+ " at " + event.getTimestamp());
eventHistory.add(event);
}
/**
* 监听用户创建事件 - 发送欢迎邮件(模拟)
*/
@EventListener(condition = "#event.type == T(com.example.demo.model.UserEvent$Type).CREATED")
@Async
public void handleUserCreated(UserEvent event) {
System.out.println("[EmailService] 发送欢迎邮件给: " + event.getUserName());
// 模拟邮件发送耗时
System.out.println("[EmailService] Simulate welcome email for " + event.getUserName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
}
System.out.println("[EmailService] 欢迎邮件发送完成");
System.out.println("[EmailService] Welcome email finished for " + event.getUserName());
}
/**
* 监听用户登录事件 - 更新登录统计
*/
@EventListener(condition = "#event.type == T(com.example.demo.model.UserEvent$Type).LOGIN")
public void handleUserLogin(UserEvent event) {
System.out.println("[LoginTracker] 记录用户登录: " + event.getUserName());
System.out.println("[LoginTracker] Track login for " + event.getUserName());
}
/**
* 监听应用启动完成事件
*/
@EventListener
public void onApplicationReady(ApplicationReadyEvent event) {
System.out.println("[EventListener] Spring Boot 应用启动完成!");
System.out.println("[EventListener] Spring Boot learning demo is ready.");
}
/**
* 获取事件历史
*/
public List<UserEvent> getEventHistory() {
return new ArrayList<>(eventHistory);
}
}
}

View File

@@ -1,55 +1,30 @@
package com.example.demo.event;
import com.example.demo.model.UserEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
/**
* 事件发布器 - 演示如何发布事件
*
* 学习点:
* - ApplicationEventPublisher 接口
* - 依赖注入发布器
* - 解耦:发布者不需要知道监听者
*/
@Component
public class UserEventPublisher {
@Autowired
private ApplicationEventPublisher eventPublisher;
/**
* 发布用户事件
*/
public void publishEvent(UserEvent event) {
System.out.println("[EventPublisher] 发布事件: " + event.getType() + " - " + event.getUserName());
System.out.println("[EventPublisher] Publish event " + event.getType() + " for " + event.getUserName());
eventPublisher.publishEvent(event);
}
/**
* 便捷方法:发布用户创建事件
*/
public void publishUserEvent(Long userId, String userName, UserEvent.Type type, String detail) {
publishEvent(new UserEvent(type, userId, userName, detail));
}
public void publishUserCreated(Long userId, String userName) {
UserEvent event = new UserEvent(
UserEvent.Type.CREATED,
userId,
userName,
"新用户注册成功"
);
publishEvent(event);
publishUserEvent(userId, userName, UserEvent.Type.CREATED, "User created successfully");
}
/**
* 便捷方法:发布用户登录事件
*/
public void publishUserLogin(Long userId, String userName) {
UserEvent event = new UserEvent(
UserEvent.Type.LOGIN,
userId,
userName,
"用户登录成功"
);
publishEvent(event);
publishUserEvent(userId, userName, UserEvent.Type.LOGIN, "User login succeeded");
}
}
}

View File

@@ -1,222 +1,229 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AOP 切面编程 - Spring Boot</title>
<title>Spring AOP Lab</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; }
.btn-primary { background: #6DB33F; color: white; }
.btn-primary:hover { background: #5da32f; }
.btn-info { background: #17a2b8; color: white; }
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; }
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; }
tr:nth-child(even) { background: #f9f9f9; }
.tip { background: #e7f3ff; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #6DB33F; }
.warn { background: #fff3cd; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #ffc107; }
.result-box { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 8px; font-family: monospace; white-space: pre-wrap; margin-top: 10px; }
.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; }
:root {
--bg: #eef7ef;
--panel: rgba(255,255,255,0.95);
--line: #d8e7d8;
--text: #102033;
--muted: #5c7184;
--brand: #3f8f2c;
--accent: #0f67b5;
--shadow: 0 20px 48px rgba(16, 32, 51, 0.12);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
color: var(--text);
background:
radial-gradient(circle at top right, rgba(63, 143, 44, 0.14), transparent 30%),
radial-gradient(circle at bottom left, rgba(15, 103, 181, 0.1), transparent 24%),
var(--bg);
}
.page { max-width: 1280px; margin: 0 auto; padding: 24px; }
.hero, .card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 28px;
box-shadow: var(--shadow);
}
.hero { padding: 28px; margin-bottom: 18px; }
.eyebrow {
display: inline-flex;
padding: 7px 12px;
border-radius: 999px;
background: rgba(63, 143, 44, 0.1);
color: var(--brand);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.1em;
text-transform: uppercase;
}
h1, h2, h3 { margin: 10px 0 12px; }
p { margin: 0; color: var(--muted); line-height: 1.8; }
.actions, .toolbar, .flow, .chips {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn, .btn-soft {
border: 0;
border-radius: 999px;
padding: 12px 18px;
font-weight: 700;
cursor: pointer;
}
.btn { background: linear-gradient(135deg, var(--brand), #62b049); color: #fff; }
.btn-soft { background: #eaf3ff; color: var(--accent); }
.grid {
display: grid;
gap: 18px;
grid-template-columns: 420px minmax(0, 1fr);
}
.card { padding: 22px; }
.list {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 14px;
}
.list-item, .flow-step {
padding: 14px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.45);
}
.flow {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
margin-top: 14px;
}
.pill {
display: inline-flex;
padding: 6px 10px;
border-radius: 999px;
background: rgba(15, 103, 181, 0.08);
color: var(--accent);
font-size: 12px;
font-weight: 700;
}
pre {
margin: 0;
min-height: 280px;
padding: 16px;
border-radius: 18px;
background: #0f1621;
color: #deebff;
white-space: pre-wrap;
font-family: Consolas, "Courier New", monospace;
overflow: auto;
}
@media (max-width: 1060px) {
.grid, .flow { grid-template-columns: 1fr; }
}
@media (max-width: 720px) {
.page { padding: 14px; }
}
</style>
</head>
<body>
<div class="nav">
<a href="/">← 返回首页</a>
<a href="/users.html">用户管理</a>
<a href="/events.html">事件机制</a>
</div>
<h1>🔪 AOP 切面编程</h1>
<div class="lab">
<h4>🧪 实验任务卡AOP</h4>
<label style="display:block;margin-bottom:8px;"><input id="aopTaskDone" type="checkbox" onchange="toggleAopTaskDone(this)"> 本任务我已经完成</label>
<ul style="padding-left:20px;line-height:1.8;">
<li>目标:观察同一请求如何触发 Before/After/Around 通知</li>
<li>步骤1调用用户接口 <code>/api/users</code></li>
<li>步骤2回到本页点击“刷新统计数据”</li>
<li>预期:统计里能看到 Controller/Service 方法耗时累积</li>
<li>常见坑:只看页面不看控制台,容易错过切面日志</li>
</ul>
</div>
<div class="card">
<h3>📊 实时性能统计</h3>
<p>AOP 自动统计所有 Controller 和 Service 方法的执行时间</p>
<button class="btn btn-primary" onclick="loadStats()">刷新统计数据</button>
<button class="btn btn-info" onclick="demoValidationError()">演示校验失败</button>
<div class="result-box" id="statsResult">点击按钮查看...</div>
</div>
<h2>📚 AOP 核心概念</h2>
<div class="card">
<h3>1. 什么是 AOP</h3>
<p>AOP (Aspect-Oriented Programming) 面向切面编程,是将<strong>横切关注点</strong><strong>业务逻辑</strong>分离的编程范式。</p>
<div class="tip">
<strong>横切关注点:</strong>日志、事务、安全、性能监控等,散布在多个模块中的公共功能。
<div class="page">
<section class="hero">
<div class="eyebrow">AOP Lab</div>
<h1>Make cross-cutting behavior visible instead of abstract.</h1>
<p>
This page focuses on the parts students usually miss: where advice wraps controller and service methods,
how timing is collected, and why a validation failure still counts as observed runtime behavior.
</p>
<div class="actions" style="margin-top:18px;">
<a class="btn-soft" href="/">Back home</a>
<a class="btn-soft" href="/users.html">Open user lab</a>
<a class="btn-soft" href="/events.html">Open event lab</a>
</div>
</section>
<div class="grid">
<aside class="card">
<div class="eyebrow">Experiment Path</div>
<h2>Recommended AOP sequence</h2>
<div class="list">
<div class="list-item">
<strong>1. Trigger controller calls</strong>
<p>Run the list and stats endpoints to make sure both controller and service methods execute.</p>
</div>
<div class="list-item">
<strong>2. Trigger a validation failure</strong>
<p>Send an invalid user payload and compare the failed request with the successful ones.</p>
</div>
<div class="list-item">
<strong>3. Inspect aspect output</strong>
<p>Load the stats endpoint and compare call counts, total time, and average time.</p>
</div>
</div>
<div class="eyebrow" style="margin-top:18px;">Advice Map</div>
<div class="chips" style="margin-top:12px;">
<span class="pill">@Before</span>
<span class="pill">@After</span>
<span class="pill">@AfterReturning</span>
<span class="pill">@AfterThrowing</span>
<span class="pill">@Around</span>
</div>
<div class="flow">
<div class="flow-step"><strong>Before</strong><p>Inspect inputs or auth context.</p></div>
<div class="flow-step"><strong>Around</strong><p>Start timer and continue the join point.</p></div>
<div class="flow-step"><strong>Controller</strong><p>Delegate work to the service.</p></div>
<div class="flow-step"><strong>Service</strong><p>Apply business logic.</p></div>
<div class="flow-step"><strong>After*</strong><p>Record result or failure details.</p></div>
</div>
</aside>
<main class="card">
<div class="eyebrow">Live Runs</div>
<h2>Run requests and inspect collected metrics</h2>
<p>The buttons below help you create real traffic, then inspect the aspect output without switching tools.</p>
<div class="toolbar" style="margin-top:14px;">
<button class="btn" type="button" onclick="loadUsers()">Load users</button>
<button class="btn-soft" type="button" onclick="loadUserStats()">Load user stats</button>
<button class="btn-soft" type="button" onclick="sendInvalidUser()">Send invalid user</button>
<button class="btn-soft" type="button" onclick="loadAopStats()">Load AOP stats</button>
</div>
<div class="list" style="margin-top:18px;">
<div class="list-item">
<strong>What to observe in the console</strong>
<p>Look for controller and service timing lines. Compare success and failure paths to see how around advice still records method duration.</p>
</div>
<div class="list-item">
<strong>What to inspect in code</strong>
<p>Read <code>PerformanceAspect</code> first, then compare it with the user endpoints that it wraps.</p>
</div>
</div>
<pre id="resultBox">Run one of the experiments above to inspect live JSON output.</pre>
</main>
</div>
<div class="card">
<h3>2. 核心术语</h3>
<table>
<tr><th>术语</th><th>说明</th></tr>
<tr><td><strong>Aspect (切面)</strong></td><td>横切关注点的模块化封装</td></tr>
<tr><td><strong>JoinPoint (连接点)</strong></td><td>程序执行的某个点(方法调用、异常抛出等)</td></tr>
<tr><td><strong>Pointcut (切入点)</strong></td><td>匹配连接点的表达式</td></tr>
<tr><td><strong>Advice (通知)</strong></td><td>在连接点执行的动作</td></tr>
<tr><td><strong>Weaving (织入)</strong></td><td>将切面应用到目标对象的过程</td></tr>
</table>
</div>
<div class="card">
<h3>3. 五种通知类型</h3>
<table>
<tr><th>注解</th><th>执行时机</th><th>用途</th></tr>
<tr><td><code>@Before</code></td><td>方法执行前</td><td>参数校验、权限检查</td></tr>
<tr><td><code>@After</code></td><td>方法执行后(无论成功或异常)</td><td>资源清理</td></tr>
<tr><td><code>@AfterReturning</code></td><td>方法成功返回后</td><td>结果处理、日志记录</td></tr>
<tr><td><code>@AfterThrowing</code></td><td>方法抛出异常后</td><td>异常处理、错误日志</td></tr>
<tr><td><code>@Around</code></td><td>环绕方法执行(最强大)</td><td>性能统计、事务管理</td></tr>
</table>
</div>
<h2>💻 代码示例</h2>
<div class="card">
<h3>日志切面示例</h3>
<pre><code>@Aspect
@Component
public class LoggingAspect {
// 切入点:匹配所有 Controller 方法
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
public void controllerMethods() {}
// 前置通知
@Before("controllerMethods()")
public void logBefore(JoinPoint jp) {
System.out.println("[AOP-Before] 方法开始: " + jp.getSignature().getName());
}
// 返回通知
@AfterReturning(pointcut = "controllerMethods()", returning = "result")
public void logAfterReturning(JoinPoint jp, Object result) {
System.out.println("[AOP-AfterReturning] 返回值: " + result);
}
}</code></pre>
</div>
<div class="card">
<h3>性能监控切面 (@Around)</h3>
<pre><code>@Aspect
@Component
public class PerformanceAspect {
@Around("execution(* com.example.demo..*.*(..))")
public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
</div>
<script>
async function renderRequest(path, options = {}) {
const box = document.getElementById('resultBox');
box.textContent = 'Loading ' + path + ' ...';
try {
Object result = pjp.proceed(); // 执行目标方法
long duration = System.currentTimeMillis() - start;
System.out.println("[AOP] " + pjp.getSignature() + " 耗时: " + duration + "ms");
return result;
} catch (Throwable e) {
System.out.println("[AOP] 方法异常: " + e.getMessage());
throw e;
const response = await fetch(path, options);
const data = await response.json();
box.textContent = JSON.stringify(data, null, 2);
} catch (error) {
box.textContent = 'Request failed: ' + error.message;
}
}
}</code></pre>
</div>
<div class="card">
<h3>切入点表达式语法</h3>
<pre><code>// 匹配任意公共方法
execution(public * *(..))
// 匹配 com.example 包下所有方法
execution(* com.example.*.*(..))
async function loadUsers() {
await renderRequest('/api/users');
}
// 匹配 Controller 层所有方法
execution(* com.example.demo.controller.*.*(..))
async function loadUserStats() {
await renderRequest('/api/users/stats');
}
// 匹配所有 Service 层的 save 开头的方法
execution(* com.example.demo.service.*.save*(..))
async function loadAopStats() {
await renderRequest('/aop/aop/stats');
}
// 匹配带有 @Service 注解的类
@within(org.springframework.stereotype.Service)
// 匹配带有自定义注解的方法
@annotation(com.example.demo.aop.RateLimited)</code></pre>
</div>
<h2>🎯 实际应用场景</h2>
<div class="card">
<table>
<tr><th>场景</th><th>实现方式</th></tr>
<tr><td>日志记录</td><td>@Before + @AfterReturning</td></tr>
<tr><td>性能监控</td><td>@Around</td></tr>
<tr><td>事务管理</td><td>@Around (Spring 已内置)</td></tr>
<tr><td>权限检查</td><td>@Before</td></tr>
<tr><td>限流控制</td><td>@Around + 自定义注解</td></tr>
<tr><td>缓存</td><td>@Around (Spring Cache)</td></tr>
</table>
</div>
<p style="margin-top: 30px;"><a href="/">← 返回学习中心</a></p>
<script>
const AOP_TASK_KEY = 'task.aop.done';
function toggleAopTaskDone(el) {
localStorage.setItem(AOP_TASK_KEY, el.checked ? '1' : '0');
}
function initAopTaskState() {
const done = localStorage.getItem(AOP_TASK_KEY) === '1';
const checkbox = document.getElementById('aopTaskDone');
if (checkbox) checkbox.checked = done;
}
async function demoValidationError() {
const box = document.getElementById('statsResult');
box.textContent = '发送错误示例请求中...';
try {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: '', email: 'bad', age: 999 })
});
const data = await res.json();
box.textContent = JSON.stringify(data, null, 2);
} catch (e) {
box.textContent = '错误: ' + e.message;
}
}
async function loadStats() {
const res = await fetch('/aop/stats');
const data = await res.json();
document.getElementById('statsResult').textContent = JSON.stringify(data, null, 2);
}
initAopTaskState();
</script>
async function sendInvalidUser() {
await renderRequest('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: '', email: 'bad', age: 999 })
});
}
</script>
</body>
</html>
</html>

View File

@@ -1,253 +1,245 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件机制 - Spring Boot</title>
<title>Spring Event Lab</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; }
:root {
--bg: #fff4ea;
--panel: rgba(255,255,255,0.95);
--line: #eed9c9;
--text: #132238;
--muted: #6a7484;
--brand: #dd6c1f;
--accent: #0f67b5;
--shadow: 0 22px 52px rgba(18, 32, 51, 0.12);
}
* { box-sizing: border-box; }
body {
margin: 0;
color: var(--text);
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
background:
radial-gradient(circle at top right, rgba(221, 108, 31, 0.16), transparent 28%),
radial-gradient(circle at bottom left, rgba(15, 103, 181, 0.1), transparent 22%),
var(--bg);
}
.page { max-width: 1280px; margin: 0 auto; padding: 24px; }
.hero, .card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 28px;
box-shadow: var(--shadow);
}
.hero { padding: 28px; margin-bottom: 18px; }
.eyebrow {
display: inline-flex;
padding: 7px 12px;
border-radius: 999px;
background: rgba(221, 108, 31, 0.1);
color: var(--brand);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.1em;
text-transform: uppercase;
}
h1, h2, h3 { margin: 10px 0 12px; }
p { margin: 0; color: var(--muted); line-height: 1.8; }
.grid {
display: grid;
gap: 18px;
grid-template-columns: 420px minmax(0, 1fr);
}
.card { padding: 22px; }
.list {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 14px;
}
.item, .timeline-step {
padding: 14px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.46);
}
.toolbar, .actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn, .btn-soft {
border: 0;
border-radius: 999px;
padding: 12px 18px;
font-weight: 700;
cursor: pointer;
}
.btn { background: linear-gradient(135deg, var(--brand), #f39a55); color: #fff; }
.btn-soft { background: #edf5ff; color: var(--accent); }
.fields {
display: grid;
gap: 12px;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin-top: 14px;
}
label {
display: flex;
flex-direction: column;
gap: 8px;
font-size: 13px;
font-weight: 700;
color: #22394f;
}
input, select {
width: 100%;
border: 1px solid var(--line);
border-radius: 14px;
padding: 12px 14px;
background: transparent;
color: var(--text);
outline: none;
}
.timeline {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-top: 14px;
}
pre {
margin: 0;
min-height: 280px;
padding: 16px;
border-radius: 18px;
background: #111926;
color: #deebff;
white-space: pre-wrap;
font-family: Consolas, "Courier New", monospace;
overflow: auto;
}
@media (max-width: 1060px) {
.grid, .timeline, .fields { grid-template-columns: 1fr; }
}
@media (max-width: 720px) {
.page { padding: 14px; }
}
</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 class="page">
<section class="hero">
<div class="eyebrow">Event Lab</div>
<h1>Understand publisher-listener decoupling through a visible event timeline.</h1>
<p>
This page helps you observe what usually stays hidden: the request can finish quickly while listeners keep
reacting in the background. Publish multiple event types and then inspect the shared event history.
</p>
<div class="actions" style="margin-top:18px;">
<a class="btn-soft" href="/">Back home</a>
<a class="btn-soft" href="/aop.html">Open AOP lab</a>
<a class="btn-soft" href="/users.html">Open user lab</a>
</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>
const EVENT_TASK_KEY = 'task.event.done';
</section>
function toggleEventTaskDone(el) {
localStorage.setItem(EVENT_TASK_KEY, el.checked ? '1' : '0');
<div class="grid">
<aside class="card">
<div class="eyebrow">Event Story</div>
<h2>What to watch for</h2>
<div class="timeline">
<div class="timeline-step"><strong>Controller</strong><p>Receives the publish request.</p></div>
<div class="timeline-step"><strong>Publisher</strong><p>Builds a `UserEvent` and emits it.</p></div>
<div class="timeline-step"><strong>Listeners</strong><p>Track history and optional async work.</p></div>
<div class="timeline-step"><strong>History API</strong><p>Lets you inspect what actually happened.</p></div>
</div>
<div class="list">
<div class="item">
<strong>Experiment 1</strong>
<p>Publish LOGIN twice with different users and confirm the history list grows without changing controller code.</p>
</div>
<div class="item">
<strong>Experiment 2</strong>
<p>Publish CREATED and then refresh history to see the same endpoint support multiple listener paths.</p>
</div>
<div class="item">
<strong>Code pairing tip</strong>
<p>Read `UserEventPublisher` and `UserEventListener` side by side to see the decoupling boundary.</p>
</div>
</div>
</aside>
<main class="card">
<div class="eyebrow">Live Event Console</div>
<h2>Publish events and inspect history</h2>
<p>Use the form to publish events, then reload history to inspect type, user, detail, and timestamp.</p>
<div class="fields">
<label>
Event type
<select id="eventType">
<option value="LOGIN">LOGIN</option>
<option value="CREATED">CREATED</option>
<option value="UPDATED">UPDATED</option>
<option value="DELETED">DELETED</option>
</select>
</label>
<label>
User id
<input id="userId" value="21">
</label>
<label>
User name
<input id="userName" value="observer-demo">
</label>
</div>
<div class="toolbar" style="margin-top:14px;">
<button class="btn" type="button" onclick="publishEvent()">Publish event</button>
<button class="btn-soft" type="button" onclick="loadHistory()">Load history</button>
<button class="btn-soft" type="button" onclick="loadInfo()">Load event notes</button>
</div>
<div class="list" style="margin-top:18px;">
<div class="item">
<strong>Interpretation hint</strong>
<p>If the controller returns immediately but history keeps growing, you are seeing decoupled follow-up behavior in action.</p>
</div>
</div>
<pre id="resultBox">Publish an event or load history to inspect live output.</pre>
</main>
</div>
</div>
<script>
async function renderRequest(path, options = {}) {
const box = document.getElementById('resultBox');
box.textContent = 'Loading ' + path + ' ...';
try {
const response = await fetch(path, options);
const data = await response.json();
box.textContent = JSON.stringify(data, null, 2);
} catch (error) {
box.textContent = 'Request failed: ' + error.message;
}
}
function initEventTaskState() {
const done = localStorage.getItem(EVENT_TASK_KEY) === '1';
const checkbox = document.getElementById('eventTaskDone');
if (checkbox) checkbox.checked = done;
}
async function publishEvent() {
const type = document.getElementById('eventType').value;
const userId = document.getElementById('userId').value.trim() || '21';
const userName = document.getElementById('userName').value.trim() || 'observer-demo';
const params = new URLSearchParams({ userId, userName, eventType: type });
await renderRequest('/aop/event/publish?' + params.toString(), { method: 'POST' });
}
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 loadHistory() {
await renderRequest('/aop/event/history');
}
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>
async function loadInfo() {
await renderRequest('/aop/event');
}
</script>
</body>
</html>
</html>

View File

@@ -3,159 +3,406 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spring Boot Learning Hub</title>
<title>Spring Boot Learning Cockpit</title>
<style>
:root {
--bg: radial-gradient(circle at top, #eef9f0 0%, #eff4ff 45%, #ffffff 100%);
--card: rgba(255,255,255,0.92);
--text: #132238;
--muted: #62788f;
--green: #3f8f2c;
--blue: #0f6db5;
--line: #dfebf6;
--shadow: 0 20px 48px rgba(16, 39, 74, 0.12);
--bg: #eef5ff;
--panel: rgba(255, 255, 255, 0.94);
--line: #d8e4f0;
--text: #122033;
--muted: #5d7288;
--brand: #177245;
--accent: #0f67b5;
--warm: #f08c2b;
--shadow: 0 22px 54px rgba(18, 32, 51, 0.12);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Segoe UI", "PingFang SC", sans-serif;
background: var(--bg);
color: var(--text);
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
background:
radial-gradient(circle at top left, rgba(23, 114, 69, 0.14), transparent 28%),
radial-gradient(circle at bottom right, rgba(15, 103, 181, 0.14), transparent 26%),
var(--bg);
}
.page {
max-width: 1120px;
margin: 0 auto;
padding: 32px 18px 48px;
}
.hero {
background: var(--card);
border: 1px solid rgba(255,255,255,0.8);
a { color: inherit; text-decoration: none; }
button, input, select { font: inherit; }
.page { max-width: 1380px; margin: 0 auto; padding: 24px; }
.hero, .card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 28px;
box-shadow: var(--shadow);
padding: 32px;
margin-bottom: 24px;
backdrop-filter: blur(10px);
}
.hero { padding: 28px; margin-bottom: 18px; }
.eyebrow {
display: inline-flex;
padding: 8px 14px;
padding: 7px 12px;
border-radius: 999px;
background: rgba(63, 143, 44, 0.12);
color: #2d6e20;
background: rgba(23, 114, 69, 0.1);
color: var(--brand);
font-size: 12px;
font-weight: 700;
letter-spacing: .06em;
font-weight: 800;
letter-spacing: 0.1em;
text-transform: uppercase;
}
h1 {
margin: 18px 0 12px;
font-size: clamp(36px, 6vw, 58px);
line-height: 1.04;
h1, h2, h3 { margin: 10px 0 12px; }
p { margin: 0; color: var(--muted); line-height: 1.8; }
.hero-grid, .workspace, .triple {
display: grid;
gap: 18px;
}
p {
color: var(--muted);
line-height: 1.7;
.hero-grid {
grid-template-columns: 1.3fr 0.9fr;
align-items: start;
}
.hero-actions {
.actions, .chip-list, .toolbar, .demo-links {
display: flex;
gap: 10px;
flex-wrap: wrap;
gap: 12px;
margin-top: 20px;
}
.btn {
.btn, .btn-soft {
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
border-radius: 999px;
padding: 12px 18px;
border-radius: 14px;
cursor: pointer;
font-weight: 700;
text-decoration: none;
}
.btn-primary { background: linear-gradient(135deg, var(--green), #57b83f); color: #fff; }
.btn-secondary { background: rgba(15, 109, 181, 0.08); color: var(--blue); }
.grid {
.btn {
color: #fff;
background: linear-gradient(135deg, var(--brand), #35a465);
}
.btn-soft {
color: var(--accent);
background: #eaf3ff;
}
.card { padding: 22px; }
.stats {
display: grid;
gap: 18px;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-top: 18px;
}
.card {
background: var(--card);
border: 1px solid rgba(255,255,255,0.8);
border-radius: 24px;
box-shadow: var(--shadow);
padding: 24px;
.stat {
padding: 16px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.5);
}
.card h2 {
margin: 0 0 10px;
font-size: 24px;
.stat span { display: block; color: var(--muted); font-size: 12px; margin-bottom: 8px; }
.stat strong { font-size: 28px; }
.workspace {
grid-template-columns: 380px minmax(0, 1fr);
align-items: start;
}
.card code {
display: inline-block;
.triple {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.flow {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 12px;
margin-top: 14px;
padding: 6px 10px;
border-radius: 10px;
background: rgba(15, 109, 181, 0.08);
color: var(--blue);
font-size: 13px;
}
.quick-links {
display: grid;
gap: 10px;
.step, .lab, .note, .code-card {
border: 1px solid var(--line);
border-radius: 20px;
padding: 16px;
background: rgba(255,255,255,0.45);
}
.step strong, .lab strong, .code-card strong { display: block; margin-bottom: 8px; }
.lab small, .step small, .note small { color: var(--muted); display: block; line-height: 1.7; }
.field {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
}
.quick-links a {
text-decoration: none;
color: var(--text);
background: rgba(255,255,255,0.8);
label { font-size: 13px; font-weight: 700; color: #21384f; }
input, select {
width: 100%;
border: 1px solid var(--line);
border-radius: 16px;
padding: 14px 16px;
font-weight: 600;
border-radius: 14px;
padding: 12px 14px;
background: transparent;
color: var(--text);
outline: none;
}
.console {
margin-top: 14px;
border-radius: 18px;
overflow: hidden;
border: 1px solid #d6e2ef;
background: #0e1723;
}
.console-head {
padding: 12px 14px;
border-bottom: 1px solid rgba(216, 228, 240, 0.14);
color: #bdd2ea;
font-weight: 700;
}
pre {
margin: 0;
min-height: 240px;
padding: 16px;
color: #dceaff;
white-space: pre-wrap;
font-family: Consolas, "Courier New", monospace;
overflow: auto;
}
.pill {
display: inline-flex;
padding: 6px 10px;
border-radius: 999px;
background: rgba(15, 103, 181, 0.08);
color: var(--accent);
font-size: 12px;
font-weight: 700;
}
.list {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 14px;
}
.list-item {
padding: 14px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.45);
}
.list-item p { margin-top: 8px; }
@media (max-width: 1180px) {
.hero-grid, .workspace, .triple, .flow, .stats { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 780px) {
.page { padding: 14px; }
.hero-grid, .workspace, .triple, .flow, .stats { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="page">
<section class="hero">
<span class="eyebrow">Spring Boot practice</span>
<h1>Explore web, validation, security, and AOP from one clean hub.</h1>
<p>
This project now highlights the most useful demos directly from the homepage so you can jump into the
user management flow, auth example, AOP pages, and health endpoints without decoding broken text first.
</p>
<div class="hero-actions">
<a class="btn btn-primary" href="/users.html">Open user management</a>
<a class="btn btn-secondary" href="/learn">View auth demo</a>
<a class="btn btn-secondary" href="/actuator/health">Check health</a>
<div class="hero-grid">
<div>
<div class="eyebrow">Spring Boot Learning Cockpit</div>
<h1>Use one workspace to understand MVC, validation, security, AOP, and application events.</h1>
<p>
This homepage is now a guided study cockpit. Instead of only linking to pages, it explains the request
path, suggests experiment sequences, and lets you call real endpoints to observe how the demo behaves.
</p>
<div class="actions" style="margin-top:18px;">
<a class="btn" href="/users.html">Open user lab</a>
<a class="btn-soft" href="/aop.html">Open AOP lab</a>
<a class="btn-soft" href="/events.html">Open event lab</a>
<a class="btn-soft" href="/learn">Open MVC learn API</a>
</div>
<div class="stats">
<div class="stat"><span>Core labs</span><strong>4</strong></div>
<div class="stat"><span>Major layers</span><strong>6</strong></div>
<div class="stat"><span>Interactive pages</span><strong>4</strong></div>
<div class="stat"><span>Tested backend paths</span><strong>2+</strong></div>
</div>
</div>
<div class="card" style="padding:0;">
<div class="card" style="box-shadow:none; border:none; border-radius:28px;">
<div class="eyebrow">Start Here</div>
<h2>Recommended study order</h2>
<div class="list">
<div class="list-item">
<strong>1. Learn endpoint binding</strong>
<p>Hit the live API explorer below and compare query, path, header, cookie, and JSON body patterns.</p>
</div>
<div class="list-item">
<strong>2. Study users and validation</strong>
<p>Open the user lab to inspect CRUD, duplicate email handling, and aggregate stats.</p>
</div>
<div class="list-item">
<strong>3. Watch cross-cutting behavior</strong>
<p>Move to AOP and events to see timing, rate limiting, and listener history.</p>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="grid">
<article class="card">
<h2>User Management</h2>
<p>CRUD, validation, duplicate email protection, search, and stats cards in one interactive page.</p>
<code>/users.html</code>
</article>
<article class="card">
<h2>AOP Showcase</h2>
<p>Review logging, performance tracing, and rate limiting demonstrations built with Spring AOP.</p>
<code>/aop.html</code>
</article>
<article class="card">
<h2>Event Flow</h2>
<p>See how controller actions can publish events and decouple downstream reactions.</p>
<code>/events.html</code>
</article>
<article class="card">
<h2>Quick Links</h2>
<div class="quick-links">
<a href="/api/users">Open `/api/users`</a>
<a href="/api/users/stats">Open `/api/users/stats`</a>
<a href="/learn">Open `/learn`</a>
<a href="/actuator/health">Open `/actuator/health`</a>
</div>
</article>
<section class="card" style="margin-bottom:18px;">
<div class="eyebrow">Architecture</div>
<h2>How one request travels through the demo</h2>
<div class="flow">
<div class="step"><strong>Browser</strong><small>Form submit or fetch call starts the request.</small></div>
<div class="step"><strong>Security</strong><small>JWT demo routes decide whether the request is public or protected.</small></div>
<div class="step"><strong>Controller</strong><small>Spring binds params, body, headers, and cookies into method arguments.</small></div>
<div class="step"><strong>Service</strong><small>Business rules run, such as duplicate email checks or stats calculation.</small></div>
<div class="step"><strong>AOP / Events</strong><small>Cross-cutting timing or event listeners react without cluttering core logic.</small></div>
<div class="step"><strong>Response</strong><small>Structured JSON or HTML returns to the page and becomes visible feedback.</small></div>
</div>
</section>
<div class="workspace">
<aside class="card">
<div class="eyebrow">Lab Tracks</div>
<h2>What to practice in each area</h2>
<div class="list">
<div class="lab">
<strong>User management</strong>
<small>Create a user, trigger duplicate email protection, search records, and compare stats before and after changes.</small>
</div>
<div class="lab">
<strong>MVC parameter binding</strong>
<small>Use `/learn` routes to compare `@RequestParam`, `@PathVariable`, `@RequestBody`, `@RequestHeader`, and `@CookieValue`.</small>
</div>
<div class="lab">
<strong>AOP tracing</strong>
<small>Trigger `/api/users` and `/api/users/stats`, then inspect `/aop/aop/stats` to see which methods were counted.</small>
</div>
<div class="lab">
<strong>Application events</strong>
<small>Publish LOGIN or CREATED events and refresh event history to understand publisher-listener decoupling.</small>
</div>
</div>
<div class="eyebrow" style="margin-top:20px;">Code Reading Map</div>
<h2>Files worth reading next</h2>
<div class="list">
<div class="code-card">
<strong>UserController -> UserService</strong>
<small>Shows validation, CRUD orchestration, search, and stats aggregation.</small>
</div>
<div class="code-card">
<strong>LearningSecurityConfig + LearningJwtFilter</strong>
<small>Explains why most labs stay public while `/api/secure/**` stays protected.</small>
</div>
<div class="code-card">
<strong>PerformanceAspect + UserEventPublisher + UserEventListener</strong>
<small>Shows the two cleanest examples of cross-cutting and event-driven behavior.</small>
</div>
</div>
</aside>
<main class="triple">
<section class="card" style="grid-column: 1 / -1;">
<div class="eyebrow">Live Explorer</div>
<h2>Call real endpoints without leaving the page</h2>
<p>Use these controls to inspect real JSON responses while you read the code. This makes the project easier to connect to concrete behavior.</p>
<div class="toolbar" style="margin-top:14px;">
<button class="btn" type="button" onclick="loadEndpoint('/actuator/health')">Health</button>
<button class="btn-soft" type="button" onclick="loadEndpoint('/api/users/stats')">User stats</button>
<button class="btn-soft" type="button" onclick="loadEndpoint('/learn')">Learn overview</button>
<button class="btn-soft" type="button" onclick="loadEndpoint('/aop/aop/stats')">AOP stats</button>
<button class="btn-soft" type="button" onclick="loadEndpoint('/aop/event/history')">Event history</button>
</div>
<div class="triple" style="margin-top:16px;">
<div class="card" style="padding:0; box-shadow:none; background:transparent; border:none;">
<div class="field">
<label for="eventType">Publish demo event</label>
<select id="eventType">
<option value="LOGIN">LOGIN</option>
<option value="CREATED">CREATED</option>
<option value="UPDATED">UPDATED</option>
<option value="DELETED">DELETED</option>
</select>
</div>
</div>
<div class="card" style="padding:0; box-shadow:none; background:transparent; border:none;">
<div class="field">
<label for="eventUserId">User id</label>
<input id="eventUserId" value="99">
</div>
</div>
<div class="card" style="padding:0; box-shadow:none; background:transparent; border:none;">
<div class="field">
<label for="eventUserName">User name</label>
<input id="eventUserName" value="learning-user">
</div>
</div>
</div>
<div class="toolbar" style="margin-top:14px;">
<button class="btn" type="button" onclick="publishEvent()">Publish event</button>
<button class="btn-soft" type="button" onclick="loadEndpoint('/api/users')">Load users</button>
</div>
<div class="console">
<div class="console-head">Endpoint output</div>
<pre id="consoleOutput">Select an experiment above to load live output.</pre>
</div>
</section>
<section class="card">
<div class="eyebrow">Experiment 1</div>
<h2>Trace validation</h2>
<p>Open the user lab, create a user, then repeat with the same email. Compare the frontend error with `DuplicateEmailException` and the global exception handler.</p>
<div class="chip-list" style="margin-top:12px;">
<span class="pill">UserController</span>
<span class="pill">UserService</span>
<span class="pill">GlobalExceptionHandler</span>
</div>
</section>
<section class="card">
<div class="eyebrow">Experiment 2</div>
<h2>Trace AOP timing</h2>
<p>Load users and stats several times, then call `/aop/aop/stats`. Watch how controller and service methods accumulate timing and call count data.</p>
<div class="chip-list" style="margin-top:12px;">
<span class="pill">PerformanceAspect</span>
<span class="pill">@Around</span>
<span class="pill">Cross-cutting</span>
</div>
</section>
<section class="card">
<div class="eyebrow">Experiment 3</div>
<h2>Trace event decoupling</h2>
<p>Publish CREATED and LOGIN events, then reload event history. This shows how the request can finish while listeners keep handling side effects.</p>
<div class="chip-list" style="margin-top:12px;">
<span class="pill">Publisher</span>
<span class="pill">Listener</span>
<span class="pill">@Async</span>
</div>
</section>
</main>
</div>
</div>
<script>
async function loadEndpoint(path, options = {}) {
const output = document.getElementById('consoleOutput');
output.textContent = 'Loading ' + path + ' ...';
try {
const response = await fetch(path, options);
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
const json = await response.json();
output.textContent = JSON.stringify(json, null, 2);
} else {
output.textContent = await response.text();
}
} catch (error) {
output.textContent = 'Request failed: ' + error.message;
}
}
async function publishEvent() {
const type = document.getElementById('eventType').value;
const userId = document.getElementById('eventUserId').value.trim() || '99';
const userName = document.getElementById('eventUserName').value.trim() || 'learning-user';
const params = new URLSearchParams({
userId,
userName,
eventType: type
});
await loadEndpoint('/aop/event/publish?' + params.toString(), { method: 'POST' });
}
</script>
</body>
</html>