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

View File

@@ -3,177 +3,178 @@ package com.example.demo.controller;
import com.example.demo.aop.PerformanceAspect; import com.example.demo.aop.PerformanceAspect;
import com.example.demo.aop.RateLimitAspect; import com.example.demo.aop.RateLimitAspect;
import com.example.demo.aop.RateLimited; import com.example.demo.aop.RateLimited;
import com.example.demo.event.UserEventListener;
import com.example.demo.event.UserEventPublisher; 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 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.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/**
* AOP 和事件机制学习控制器
*/
@RestController @RestController
@RequestMapping("/aop") @RequestMapping("/aop")
public class AopEventController { public class AopEventController {
private final UserService userService; private final UserService userService;
private final UserEventPublisher eventPublisher; private final UserEventPublisher eventPublisher;
private final UserEventListener userEventListener;
private final PerformanceAspect performanceAspect; private final PerformanceAspect performanceAspect;
private final RateLimitAspect rateLimitAspect; private final RateLimitAspect rateLimitAspect;
public AopEventController(UserService userService, public AopEventController(UserService userService,
UserEventPublisher eventPublisher, UserEventPublisher eventPublisher,
UserEventListener userEventListener,
PerformanceAspect performanceAspect, PerformanceAspect performanceAspect,
RateLimitAspect rateLimitAspect) { RateLimitAspect rateLimitAspect) {
this.userService = userService; this.userService = userService;
this.eventPublisher = eventPublisher; this.eventPublisher = eventPublisher;
this.userEventListener = userEventListener;
this.performanceAspect = performanceAspect; this.performanceAspect = performanceAspect;
this.rateLimitAspect = rateLimitAspect; this.rateLimitAspect = rateLimitAspect;
} }
/**
* 学习首页
*/
@GetMapping @GetMapping
public Map<String, Object> index() { public Map<String, Object> index() {
Map<String, Object> info = new HashMap<>(); 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[]{ info.put("topics", new String[]{
"AOP 切面编程", "AOP advice and performance tracing",
"事件机制", "Application events and listener decoupling",
"Bean 生命周期" "Annotation-driven rate limiting"
}); });
info.put("endpoints", new String[]{ info.put("endpoints", new String[]{
"GET /aop - AOP 概念说明", "GET /aop/aop",
"GET /aop/stats - 性能统计", "GET /aop/aop/stats",
"GET /aop/event - 事件机制说明", "GET /aop/event",
"POST /aop/event/publish - 发布用户事件", "POST /aop/event/publish",
"GET /aop/event/history - 查看事件历史", "GET /aop/event/history",
"GET /aop/ratelimit - 限流测试" "GET /aop/ratelimit"
}); });
info.put("userCount", userService.findAll().size());
return info; return info;
} }
// ==================== AOP 示例 ====================
/**
* AOP 概念说明
*/
@GetMapping("/aop") @GetMapping("/aop")
public Map<String, Object> aopInfo() { public Map<String, Object> aopInfo() {
Map<String, Object> info = new HashMap<>(); Map<String, Object> info = new HashMap<>();
info.put("title", "AOP 切面编程"); info.put("title", "AOP learning notes");
info.put("concepts", new String[]{ info.put("concepts", new String[]{
"Aspect(切面): 横切关注点的模块化", "Aspect: a reusable cross-cutting module",
"JoinPoint(连接点): 程序执行的某个点", "Join point: a place where the program can be intercepted",
"Pointcut(切入点): 匹配连接点的表达式", "Pointcut: the matcher that selects join points",
"Advice(通知): 在连接点执行的动作", "Advice: code that runs before, after, or around the join point",
"Weaving(织入): 将切面应用到目标对象" "Weaving: combining the aspect with the target object"
}); });
info.put("adviceTypes", new String[]{ info.put("adviceTypes", new String[]{
"@Before - 方法执行前", "@Before for pre-checks",
"@After - 方法执行后(无论成功或异常)", "@After for cleanup",
"@AfterReturning - 方法成功返回后", "@AfterReturning for successful results",
"@AfterThrowing - 方法抛出异常后", "@AfterThrowing for failures",
"@Around - 环绕通知(最强大)" "@Around for timing, wrapping, and total control"
}); });
info.put("useCases", new String[]{ info.put("useCases", new String[]{
"日志记录", "Logging",
"性能监控", "Performance measurement",
"事务管理", "Authorization checks",
"权限检查", "Rate limiting",
"限流控制" "Reusable validation"
}); });
return info; return info;
} }
/**
* 查看性能统计
*/
@GetMapping("/aop/stats") @GetMapping("/aop/stats")
public Map<String, Object> getPerformanceStats() { public Map<String, Object> getPerformanceStats() {
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("title", "方法性能统计"); result.put("title", "Collected performance metrics");
result.put("description", "由 PerformanceAspect 自动收集"); result.put("description", "These numbers come from the around advice on controllers and services.");
result.put("statistics", performanceAspect.getStatistics()); result.put("statistics", performanceAspect.getStatistics());
return result; return result;
} }
// ==================== 事件机制示例 ====================
/**
* 事件机制说明
*/
@GetMapping("/event") @GetMapping("/event")
public Map<String, Object> eventInfo() { public Map<String, Object> eventInfo() {
Map<String, Object> info = new HashMap<>(); Map<String, Object> info = new HashMap<>();
info.put("title", "Spring 事件机制"); info.put("title", "Spring application events");
info.put("concepts", new String[]{ info.put("concepts", new String[]{
"ApplicationEventPublisher - 事件发布者", "ApplicationEventPublisher emits a domain or lifecycle event.",
"@EventListener - 事件监听器", "@EventListener reacts without tight coupling to the caller.",
"@Async - 异步处理事件", "@Async lets slow follow-up work happen outside the request path.",
"condition - 条件过滤" "Conditions help one listener focus on one event type."
}); });
info.put("benefits", new String[]{ info.put("benefits", new String[]{
"解耦:发布者和监听者互不依赖", "Controllers stay smaller.",
"扩展:新增监听器无需修改发布者", "Listeners can grow independently.",
"异步:耗时操作不阻塞主流程", "The request flow stays responsive.",
"测试:更容易进行单元测试" "Side effects become easier to test."
});
info.put("eventTypes", new String[]{
"CREATED - 用户创建",
"UPDATED - 用户更新",
"DELETED - 用户删除",
"LOGIN - 用户登录"
}); });
info.put("eventTypes", UserEvent.Type.values());
return info; return info;
} }
/**
* 发布用户事件
*/
@PostMapping("/event/publish") @PostMapping("/event/publish")
public Map<String, Object> publishEvent( public Map<String, Object> publishEvent(
@RequestParam Long userId, @RequestParam Long userId,
@RequestParam String userName, @RequestParam String userName,
@RequestParam(defaultValue = "LOGIN") String eventType @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<>(); Map<String, Object> result = new HashMap<>();
result.put("message", "事件已发布"); result.put("message", "Event published successfully.");
result.put("eventType", eventType); result.put("eventType", type.name());
result.put("userId", userId); result.put("userId", userId);
result.put("userName", userName); result.put("userName", userName);
result.put("historySize", userEventListener.getEventHistory().size());
return result; return result;
} }
/**
* 查看事件历史
*/
@GetMapping("/event/history") @GetMapping("/event/history")
public Map<String, Object> getEventHistory() { 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<>(); Map<String, Object> result = new HashMap<>();
result.put("title", "事件历史记录"); result.put("title", "Recent user event history");
result.put("note", "由 UserEventListener 自动记录"); result.put("total", items.size());
result.put("items", items);
return result; return result;
} }
// ==================== 限流示例 ====================
/**
* 限流测试接口
*/
@GetMapping("/ratelimit") @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() { public Map<String, Object> testRateLimit() {
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("message", "请求成功"); result.put("message", "Rate-limited endpoint executed successfully.");
result.put("note", "使用 @RateLimited 注解"); result.put("note", "The @RateLimited annotation wraps this method through an aspect.");
result.put("rateLimitStatus", rateLimitAspect.getRateLimitStatus()); result.put("rateLimitStatus", rateLimitAspect.getRateLimitStatus());
return result; 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; package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Value; 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.HashMap;
import java.util.Map; import java.util.Map;
/**
* 学习示例控制器
*
* 学习点:
* - 各种参数接收方式
* - 配置注入
* - 响应格式
*/
@RestController @RestController
public class LearnController { public class LearnController {
// 从配置文件注入值
@Value("${spring.application.name:demo}") @Value("${spring.application.name:demo}")
private String appName; private String appName;
/** @GetMapping("/learn/root")
* 根路径 - 重定向到学习中心
*/
@GetMapping("/")
public Map<String, Object> root() { public Map<String, Object> root() {
Map<String, Object> info = new HashMap<>(); Map<String, Object> info = new HashMap<>();
info.put("message", "欢迎来到 Spring Boot 学习脚手架!"); info.put("message", "Welcome to the Spring Boot learning workspace.");
info.put("learn", "https://spring.xiaoxiaoluohao.indevs.in/learn"); info.put("homePage", "/");
info.put("aop", "https://spring.xiaoxiaoluohao.indevs.in/aop"); info.put("userLab", "/users.html");
info.put("api", "https://spring.xiaoxiaoluohao.indevs.in/api/users"); info.put("aopLab", "/aop.html");
info.put("eventLab", "/events.html");
info.put("learnApi", "/learn");
return info; return info;
} }
// GET /learn - API 信息
@GetMapping("/learn") @GetMapping("/learn")
public Map<String, Object> info() { public Map<String, Object> info() {
Map<String, Object> info = new HashMap<>(); Map<String, Object> info = new HashMap<>();
info.put("app", appName); 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[]{ info.put("endpoints", new String[]{
"GET /learn/params?name=xxx&age=18 - 参数示例", "GET /learn/params?name=alex&age=18",
"POST /learn/body - JSON 请求体示例", "POST /learn/body",
"GET /learn/path/{id} - 路径变量示例", "GET /learn/path/{id}",
"GET /learn/header - 请求头示例", "GET /learn/header",
"GET /learn/cookie - Cookie 示例", "GET /learn/cookie",
"POST /api/auth/login - 学习用 JWT 登录", "GET /learn/exception",
"GET /api/secure/me - 受保护接口(需 Bearer Token" "POST /api/auth/login",
"GET /api/secure/me"
}); });
return info; return info;
} }
// GET /learn/params?name=xxx&age=18 - 查询参数
@GetMapping("/learn/params") @GetMapping("/learn/params")
public Map<String, Object> params( public Map<String, Object> params(
@RequestParam(required = false, defaultValue = "游客") String name, @RequestParam(required = false, defaultValue = "guest") String name,
@RequestParam(required = false, defaultValue = "0") Integer age @RequestParam(required = false, defaultValue = "0") Integer age
) { ) {
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("pattern", "@RequestParam");
result.put("name", name); result.put("name", name);
result.put("age", age); result.put("age", age);
result.put("tip", "使用 @RequestParam 接收查询参数"); result.put("tip", "Query parameters are useful for filters, pagination, and optional toggles.");
return result; return result;
} }
// POST /learn/body - 请求体
@PostMapping("/learn/body") @PostMapping("/learn/body")
public Map<String, Object> body(@RequestBody Map<String, Object> data) { public Map<String, Object> body(@RequestBody Map<String, Object> data) {
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("pattern", "@RequestBody");
result.put("received", data); 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; return result;
} }
// GET /learn/path/{id} - 路径变量
@GetMapping("/learn/path/{id}") @GetMapping("/learn/path/{id}")
public Map<String, Object> path(@PathVariable String id) { public Map<String, Object> path(@PathVariable String id) {
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("pattern", "@PathVariable");
result.put("id", id); 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; return result;
} }
// GET /learn/header - 请求头
@GetMapping("/learn/header") @GetMapping("/learn/header")
public Map<String, Object> header(@RequestHeader(value = "User-Agent", required = false) String userAgent) { public Map<String, Object> header(@RequestHeader(value = "User-Agent", required = false) String userAgent) {
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("pattern", "@RequestHeader");
result.put("userAgent", userAgent); result.put("userAgent", userAgent);
result.put("tip", "使用 @RequestHeader 获取请求头"); result.put("tip", "Headers often carry auth context, tracing ids, and client metadata.");
return result; return result;
} }
// GET /learn/cookie - Cookie
@GetMapping("/learn/cookie") @GetMapping("/learn/cookie")
public Map<String, Object> cookie(@CookieValue(value = "JSESSIONID", required = false) String sessionId) { public Map<String, Object> cookie(@CookieValue(value = "JSESSIONID", required = false) String sessionId) {
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("pattern", "@CookieValue");
result.put("sessionId", sessionId); 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; return result;
} }
// GET /learn/exception - 异常处理
@GetMapping("/learn/exception") @GetMapping("/learn/exception")
public String exception() { public String exception() {
throw new RuntimeException("这是一个测试异常"); throw new RuntimeException("Intentional demo exception from /learn/exception");
} }
// 全局异常处理
@ExceptionHandler(RuntimeException.class) @ExceptionHandler(RuntimeException.class)
public Map<String, Object> handleException(RuntimeException e) { public Map<String, Object> handleException(RuntimeException e) {
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("pattern", "@ExceptionHandler");
result.put("error", e.getMessage()); 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; return result;
} }
} }

View File

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

View File

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

View File

@@ -1,222 +1,229 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AOP 切面编程 - Spring Boot</title> <title>Spring AOP Lab</title>
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } :root {
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #f5f5f5; } --bg: #eef7ef;
h1 { color: #6DB33F; margin: 20px 0; } --panel: rgba(255,255,255,0.95);
h2 { color: #333; border-bottom: 2px solid #6DB33F; padding-bottom: 10px; margin: 20px 0 15px; } --line: #d8e7d8;
h3 { color: #6DB33F; margin: 15px 0 10px; } --text: #102033;
.card { background: white; padding: 25px; margin: 15px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } --muted: #5c7184;
.btn { display: inline-block; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 500; cursor: pointer; border: none; } --brand: #3f8f2c;
.btn-primary { background: #6DB33F; color: white; } --accent: #0f67b5;
.btn-primary:hover { background: #5da32f; } --shadow: 0 20px 48px rgba(16, 32, 51, 0.12);
.btn-info { background: #17a2b8; color: white; } }
code { background: #f0f0f0; padding: 2px 8px; border-radius: 4px; font-family: 'Fira Code', monospace; } * { box-sizing: border-box; }
pre { background: #2d2d2d; color: #f8f8f2; padding: 20px; border-radius: 8px; overflow-x: auto; margin: 15px 0; } body {
pre code { background: none; color: inherit; } margin: 0;
table { width: 100%; border-collapse: collapse; margin: 15px 0; } font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; } color: var(--text);
th { background: #6DB33F; color: white; } background:
tr:nth-child(even) { background: #f9f9f9; } radial-gradient(circle at top right, rgba(63, 143, 44, 0.14), transparent 30%),
.tip { background: #e7f3ff; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #6DB33F; } radial-gradient(circle at bottom left, rgba(15, 103, 181, 0.1), transparent 24%),
.warn { background: #fff3cd; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #ffc107; } var(--bg);
.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; } .page { max-width: 1280px; margin: 0 auto; padding: 24px; }
.nav a { margin-right: 15px; color: #6DB33F; text-decoration: none; } .hero, .card {
.nav a:hover { text-decoration: underline; } background: var(--panel);
.lab { background:#fff7e6; border-left:4px solid #fa8c16; padding:15px; border-radius:8px; margin:15px 0; } border: 1px solid var(--line);
.lab h4 { color:#ad6800; margin-bottom:8px; } 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> </style>
</head> </head>
<body> <body>
<div class="nav"> <div class="page">
<a href="/">← 返回首页</a> <section class="hero">
<a href="/users.html">用户管理</a> <div class="eyebrow">AOP Lab</div>
<a href="/events.html">事件机制</a> <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> </div>
</section>
<h1>🔪 AOP 切面编程</h1> <div class="grid">
<aside class="card">
<div class="lab"> <div class="eyebrow">Experiment Path</div>
<h4>🧪 实验任务卡AOP</h4> <h2>Recommended AOP sequence</h2>
<label style="display:block;margin-bottom:8px;"><input id="aopTaskDone" type="checkbox" onchange="toggleAopTaskDone(this)"> 本任务我已经完成</label> <div class="list">
<ul style="padding-left:20px;line-height:1.8;"> <div class="list-item">
<li>目标:观察同一请求如何触发 Before/After/Around 通知</li> <strong>1. Trigger controller calls</strong>
<li>步骤1调用用户接口 <code>/api/users</code></li> <p>Run the list and stats endpoints to make sure both controller and service methods execute.</p>
<li>步骤2回到本页点击“刷新统计数据”</li>
<li>预期:统计里能看到 Controller/Service 方法耗时累积</li>
<li>常见坑:只看页面不看控制台,容易错过切面日志</li>
</ul>
</div> </div>
<div class="list-item">
<div class="card"> <strong>2. Trigger a validation failure</strong>
<h3>📊 实时性能统计</h3> <p>Send an invalid user payload and compare the failed request with the successful ones.</p>
<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> </div>
<div class="list-item">
<h2>📚 AOP 核心概念</h2> <strong>3. Inspect aspect output</strong>
<p>Load the stats endpoint and compare call counts, total time, and average time.</p>
<div class="card">
<h3>1. 什么是 AOP</h3>
<p>AOP (Aspect-Oriented Programming) 面向切面编程,是将<strong>横切关注点</strong><strong>业务逻辑</strong>分离的编程范式。</p>
<div class="tip">
<strong>横切关注点:</strong>日志、事务、安全、性能监控等,散布在多个模块中的公共功能。
</div> </div>
</div> </div>
<div class="card"> <div class="eyebrow" style="margin-top:18px;">Advice Map</div>
<h3>2. 核心术语</h3> <div class="chips" style="margin-top:12px;">
<table> <span class="pill">@Before</span>
<tr><th>术语</th><th>说明</th></tr> <span class="pill">@After</span>
<tr><td><strong>Aspect (切面)</strong></td><td>横切关注点的模块化封装</td></tr> <span class="pill">@AfterReturning</span>
<tr><td><strong>JoinPoint (连接点)</strong></td><td>程序执行的某个点(方法调用、异常抛出等)</td></tr> <span class="pill">@AfterThrowing</span>
<tr><td><strong>Pointcut (切入点)</strong></td><td>匹配连接点的表达式</td></tr> <span class="pill">@Around</span>
<tr><td><strong>Advice (通知)</strong></td><td>在连接点执行的动作</td></tr>
<tr><td><strong>Weaving (织入)</strong></td><td>将切面应用到目标对象的过程</td></tr>
</table>
</div> </div>
<div class="card"> <div class="flow">
<h3>3. 五种通知类型</h3> <div class="flow-step"><strong>Before</strong><p>Inspect inputs or auth context.</p></div>
<table> <div class="flow-step"><strong>Around</strong><p>Start timer and continue the join point.</p></div>
<tr><th>注解</th><th>执行时机</th><th>用途</th></tr> <div class="flow-step"><strong>Controller</strong><p>Delegate work to the service.</p></div>
<tr><td><code>@Before</code></td><td>方法执行前</td><td>参数校验、权限检查</td></tr> <div class="flow-step"><strong>Service</strong><p>Apply business logic.</p></div>
<tr><td><code>@After</code></td><td>方法执行后(无论成功或异常)</td><td>资源清理</td></tr> <div class="flow-step"><strong>After*</strong><p>Record result or failure details.</p></div>
<tr><td><code>@AfterReturning</code></td><td>方法成功返回后</td><td>结果处理、日志记录</td></tr> </div>
<tr><td><code>@AfterThrowing</code></td><td>方法抛出异常后</td><td>异常处理、错误日志</td></tr> </aside>
<tr><td><code>@Around</code></td><td>环绕方法执行(最强大)</td><td>性能统计、事务管理</td></tr>
</table> <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>
<h2>💻 代码示例</h2> <div class="list" style="margin-top:18px;">
<div class="list-item">
<div class="card"> <strong>What to observe in the console</strong>
<h3>日志切面示例</h3> <p>Look for controller and service timing lines. Compare success and failure paths to see how around advice still records method duration.</p>
<pre><code>@Aspect </div>
@Component <div class="list-item">
public class LoggingAspect { <strong>What to inspect in code</strong>
<p>Read <code>PerformanceAspect</code> first, then compare it with the user endpoints that it wraps.</p>
// 切入点:匹配所有 Controller 方法 </div>
@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>
<div class="card"> <pre id="resultBox">Run one of the experiments above to inspect live JSON output.</pre>
<h3>性能监控切面 (@Around)</h3> </main>
<pre><code>@Aspect </div>
@Component </div>
public class PerformanceAspect {
@Around("execution(* com.example.demo..*.*(..))")
public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
<script>
async function renderRequest(path, options = {}) {
const box = document.getElementById('resultBox');
box.textContent = 'Loading ' + path + ' ...';
try { try {
Object result = pjp.proceed(); // 执行目标方法 const response = await fetch(path, options);
long duration = System.currentTimeMillis() - start; const data = await response.json();
System.out.println("[AOP] " + pjp.getSignature() + " 耗时: " + duration + "ms"); box.textContent = JSON.stringify(data, null, 2);
return result; } catch (error) {
} catch (Throwable e) { box.textContent = 'Request failed: ' + error.message;
System.out.println("[AOP] 方法异常: " + e.getMessage());
throw e;
} }
} }
}</code></pre>
</div>
<div class="card"> async function loadUsers() {
<h3>切入点表达式语法</h3> await renderRequest('/api/users');
<pre><code>// 匹配任意公共方法
execution(public * *(..))
// 匹配 com.example 包下所有方法
execution(* com.example.*.*(..))
// 匹配 Controller 层所有方法
execution(* com.example.demo.controller.*.*(..))
// 匹配所有 Service 层的 save 开头的方法
execution(* com.example.demo.service.*.save*(..))
// 匹配带有 @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() { async function loadUserStats() {
const done = localStorage.getItem(AOP_TASK_KEY) === '1'; await renderRequest('/api/users/stats');
const checkbox = document.getElementById('aopTaskDone');
if (checkbox) checkbox.checked = done;
} }
async function demoValidationError() { async function loadAopStats() {
const box = document.getElementById('statsResult'); await renderRequest('/aop/aop/stats');
box.textContent = '发送错误示例请求中...'; }
try {
const res = await fetch('/api/users', { async function sendInvalidUser() {
await renderRequest('/api/users', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: '', email: 'bad', age: 999 }) 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;
} }
} </script>
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>
</body> </body>
</html> </html>

View File

@@ -1,253 +1,245 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件机制 - Spring Boot</title> <title>Spring Event Lab</title>
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } :root {
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #f5f5f5; } --bg: #fff4ea;
h1 { color: #6DB33F; margin: 20px 0; } --panel: rgba(255,255,255,0.95);
h2 { color: #333; border-bottom: 2px solid #6DB33F; padding-bottom: 10px; margin: 20px 0 15px; } --line: #eed9c9;
h3 { color: #6DB33F; margin: 15px 0 10px; } --text: #132238;
.card { background: white; padding: 25px; margin: 15px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } --muted: #6a7484;
.btn { display: inline-block; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 500; cursor: pointer; border: none; margin: 5px; } --brand: #dd6c1f;
.btn-primary { background: #6DB33F; color: white; } --accent: #0f67b5;
.btn-primary:hover { background: #5da32f; } --shadow: 0 22px 52px rgba(18, 32, 51, 0.12);
.btn-info { background: #17a2b8; color: white; } }
.btn-warning { background: #ffc107; color: #333; } * { box-sizing: border-box; }
code { background: #f0f0f0; padding: 2px 8px; border-radius: 4px; font-family: 'Fira Code', monospace; } body {
pre { background: #2d2d2d; color: #f8f8f2; padding: 20px; border-radius: 8px; overflow-x: auto; margin: 15px 0; } margin: 0;
pre code { background: none; color: inherit; } color: var(--text);
.tip { background: #e7f3ff; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #6DB33F; } font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
.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; } background:
table { width: 100%; border-collapse: collapse; margin: 15px 0; } radial-gradient(circle at top right, rgba(221, 108, 31, 0.16), transparent 28%),
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; } radial-gradient(circle at bottom left, rgba(15, 103, 181, 0.1), transparent 22%),
th { background: #6DB33F; color: white; } var(--bg);
.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; } .page { max-width: 1280px; margin: 0 auto; padding: 24px; }
.event-flow .arrow { font-size: 24px; color: #6DB33F; } .hero, .card {
.nav { margin-bottom: 20px; } background: var(--panel);
.nav a { margin-right: 15px; color: #6DB33F; text-decoration: none; } border: 1px solid var(--line);
.nav a:hover { text-decoration: underline; } border-radius: 28px;
.lab { background:#fff7e6; border-left:4px solid #fa8c16; padding:15px; border-radius:8px; margin:15px 0; } box-shadow: var(--shadow);
.lab h4 { color:#ad6800; margin-bottom:8px; } }
.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> </style>
</head> </head>
<body> <body>
<div class="nav"> <div class="page">
<a href="/">← 返回首页</a> <section class="hero">
<a href="/users.html">用户管理</a> <div class="eyebrow">Event Lab</div>
<a href="/aop.html">AOP 切面</a> <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>
</section>
<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>
<h1>📡 Spring 事件机制</h1> <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>
<div class="lab"> <main class="card">
<h4>🧪 实验任务卡(事件)</h4> <div class="eyebrow">Live Event Console</div>
<label style="display:block;margin-bottom:8px;"><input id="eventTaskDone" type="checkbox" onchange="toggleEventTaskDone(this)"> 本任务我已经完成</label> <h2>Publish events and inspect history</h2>
<ul style="padding-left:20px;line-height:1.8;"> <p>Use the form to publish events, then reload history to inspect type, user, detail, and timestamp.</p>
<li>目标:体验发布者与监听者解耦</li>
<li>步骤1输入 userId/userName点击“发布登录事件”</li> <div class="fields">
<li>步骤2重复发布不同用户比较返回结果</li> <label>
<li>预期:接口立即返回;监听处理在日志中可观察</li> Event type
<li>常见坑:把事件当同步 RPC忽略异步监听特性</li> <select id="eventType">
</ul> <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>
<div class="card"> <div class="toolbar" style="margin-top:14px;">
<h3>🎉 事件发布演示</h3> <button class="btn" type="button" onclick="publishEvent()">Publish event</button>
<p>模拟用户登录事件,观察事件发布和监听过程</p> <button class="btn-soft" type="button" onclick="loadHistory()">Load history</button>
<div style="margin: 15px 0;"> <button class="btn-soft" type="button" onclick="loadInfo()">Load event notes</button>
<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> </div>
<h2>🔄 事件机制流程</h2> <div class="list" style="margin-top:18px;">
<div class="item">
<div class="event-flow"> <strong>Interpretation hint</strong>
<span>发布者</span> <p>If the controller returns immediately but history keeps growing, you are seeing decoupled follow-up behavior in action.</p>
<span class="arrow"></span> </div>
<span>ApplicationEventPublisher</span>
<span class="arrow"></span>
<span>事件</span>
<span class="arrow"></span>
<span>监听者</span>
</div> </div>
<div class="tip"> <pre id="resultBox">Publish an event or load history to inspect live output.</pre>
<strong>核心优势:</strong> </main>
<ul style="margin-top: 10px; padding-left: 20px;">
<li><strong>解耦:</strong>发布者和监听者互不依赖</li>
<li><strong>扩展:</strong>新增监听器无需修改发布者</li>
<li><strong>异步:</strong>耗时操作不阻塞主流程</li>
</ul>
</div> </div>
</div>
<h2>💻 代码实现</h2> <script>
async function renderRequest(path, options = {}) {
<div class="card"> const box = document.getElementById('resultBox');
<h3>1. 定义事件</h3> box.textContent = 'Loading ' + path + ' ...';
<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';
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 { try {
const res = await fetch('/aop/event/publish?userName=', { method: 'POST' }); const response = await fetch(path, options);
const data = await res.json(); const data = await response.json();
resultBox.textContent = JSON.stringify(data, null, 2); box.textContent = JSON.stringify(data, null, 2);
} catch (e) { } catch (error) {
resultBox.textContent = '错误: ' + e.message; box.textContent = 'Request failed: ' + error.message;
} }
} }
async function publishEvent() { async function publishEvent() {
const userId = document.getElementById('userId').value; const type = document.getElementById('eventType').value;
const userName = document.getElementById('userName').value; const userId = document.getElementById('userId').value.trim() || '21';
const userName = document.getElementById('userName').value.trim() || 'observer-demo';
try { const params = new URLSearchParams({ userId, userName, eventType: type });
const res = await fetch(`/aop/event/publish?userId=${userId}&userName=${encodeURIComponent(userName)}`, { await renderRequest('/aop/event/publish?' + params.toString(), { method: 'POST' });
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(); async function loadHistory() {
</script> await renderRequest('/aop/event/history');
}
async function loadInfo() {
await renderRequest('/aop/event');
}
</script>
</body> </body>
</html> </html>

View File

@@ -3,159 +3,406 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spring Boot Learning Hub</title> <title>Spring Boot Learning Cockpit</title>
<style> <style>
:root { :root {
--bg: radial-gradient(circle at top, #eef9f0 0%, #eff4ff 45%, #ffffff 100%); --bg: #eef5ff;
--card: rgba(255,255,255,0.92); --panel: rgba(255, 255, 255, 0.94);
--text: #132238; --line: #d8e4f0;
--muted: #62788f; --text: #122033;
--green: #3f8f2c; --muted: #5d7288;
--blue: #0f6db5; --brand: #177245;
--line: #dfebf6; --accent: #0f67b5;
--shadow: 0 20px 48px rgba(16, 39, 74, 0.12); --warm: #f08c2b;
--shadow: 0 22px 54px rgba(18, 32, 51, 0.12);
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
body { body {
margin: 0; margin: 0;
font-family: "Segoe UI", "PingFang SC", sans-serif;
background: var(--bg);
color: var(--text); 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 { a { color: inherit; text-decoration: none; }
max-width: 1120px; button, input, select { font: inherit; }
margin: 0 auto; .page { max-width: 1380px; margin: 0 auto; padding: 24px; }
padding: 32px 18px 48px; .hero, .card {
} background: var(--panel);
.hero { border: 1px solid var(--line);
background: var(--card);
border: 1px solid rgba(255,255,255,0.8);
border-radius: 28px; border-radius: 28px;
box-shadow: var(--shadow); box-shadow: var(--shadow);
padding: 32px; backdrop-filter: blur(10px);
margin-bottom: 24px;
} }
.hero { padding: 28px; margin-bottom: 18px; }
.eyebrow { .eyebrow {
display: inline-flex; display: inline-flex;
padding: 8px 14px; padding: 7px 12px;
border-radius: 999px; border-radius: 999px;
background: rgba(63, 143, 44, 0.12); background: rgba(23, 114, 69, 0.1);
color: #2d6e20; color: var(--brand);
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 800;
letter-spacing: .06em; letter-spacing: 0.1em;
text-transform: uppercase; text-transform: uppercase;
} }
h1 { h1, h2, h3 { margin: 10px 0 12px; }
margin: 18px 0 12px; p { margin: 0; color: var(--muted); line-height: 1.8; }
font-size: clamp(36px, 6vw, 58px); .hero-grid, .workspace, .triple {
line-height: 1.04; display: grid;
gap: 18px;
} }
p { .hero-grid {
color: var(--muted); grid-template-columns: 1.3fr 0.9fr;
line-height: 1.7; align-items: start;
} }
.hero-actions { .actions, .chip-list, .toolbar, .demo-links {
display: flex; display: flex;
gap: 10px;
flex-wrap: wrap; flex-wrap: wrap;
gap: 12px;
margin-top: 20px;
} }
.btn { .btn, .btn-soft {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 0;
border-radius: 999px;
padding: 12px 18px; padding: 12px 18px;
border-radius: 14px; cursor: pointer;
font-weight: 700; font-weight: 700;
text-decoration: none;
} }
.btn-primary { background: linear-gradient(135deg, var(--green), #57b83f); color: #fff; } .btn {
.btn-secondary { background: rgba(15, 109, 181, 0.08); color: var(--blue); } color: #fff;
.grid { background: linear-gradient(135deg, var(--brand), #35a465);
}
.btn-soft {
color: var(--accent);
background: #eaf3ff;
}
.card { padding: 22px; }
.stats {
display: grid; display: grid;
gap: 18px; grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px;
margin-top: 18px;
} }
.card { .stat {
background: var(--card); padding: 16px;
border: 1px solid rgba(255,255,255,0.8); border-radius: 18px;
border-radius: 24px; border: 1px solid var(--line);
box-shadow: var(--shadow); background: rgba(255,255,255,0.5);
padding: 24px;
} }
.card h2 { .stat span { display: block; color: var(--muted); font-size: 12px; margin-bottom: 8px; }
margin: 0 0 10px; .stat strong { font-size: 28px; }
font-size: 24px; .workspace {
grid-template-columns: 380px minmax(0, 1fr);
align-items: start;
} }
.card code { .triple {
display: inline-block; grid-template-columns: repeat(3, minmax(0, 1fr));
}
.flow {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 12px;
margin-top: 14px; margin-top: 14px;
padding: 6px 10px;
border-radius: 10px;
background: rgba(15, 109, 181, 0.08);
color: var(--blue);
font-size: 13px;
} }
.quick-links { .step, .lab, .note, .code-card {
display: grid; border: 1px solid var(--line);
gap: 10px; 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; margin-top: 12px;
} }
.quick-links a { label { font-size: 13px; font-weight: 700; color: #21384f; }
text-decoration: none; input, select {
color: var(--text); width: 100%;
background: rgba(255,255,255,0.8);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 16px; border-radius: 14px;
padding: 14px 16px; padding: 12px 14px;
font-weight: 600; 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> </style>
</head> </head>
<body> <body>
<div class="page"> <div class="page">
<section class="hero"> <section class="hero">
<span class="eyebrow">Spring Boot practice</span> <div class="hero-grid">
<h1>Explore web, validation, security, and AOP from one clean hub.</h1> <div>
<div class="eyebrow">Spring Boot Learning Cockpit</div>
<h1>Use one workspace to understand MVC, validation, security, AOP, and application events.</h1>
<p> <p>
This project now highlights the most useful demos directly from the homepage so you can jump into the This homepage is now a guided study cockpit. Instead of only linking to pages, it explains the request
user management flow, auth example, AOP pages, and health endpoints without decoding broken text first. path, suggests experiment sequences, and lets you call real endpoints to observe how the demo behaves.
</p> </p>
<div class="hero-actions"> <div class="actions" style="margin-top:18px;">
<a class="btn btn-primary" href="/users.html">Open user management</a> <a class="btn" href="/users.html">Open user lab</a>
<a class="btn btn-secondary" href="/learn">View auth demo</a> <a class="btn-soft" href="/aop.html">Open AOP lab</a>
<a class="btn btn-secondary" href="/actuator/health">Check health</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> </div>
</section> </section>
<section class="grid"> <section class="card" style="margin-bottom:18px;">
<article class="card"> <div class="eyebrow">Architecture</div>
<h2>User Management</h2> <h2>How one request travels through the demo</h2>
<p>CRUD, validation, duplicate email protection, search, and stats cards in one interactive page.</p> <div class="flow">
<code>/users.html</code> <div class="step"><strong>Browser</strong><small>Form submit or fetch call starts the request.</small></div>
</article> <div class="step"><strong>Security</strong><small>JWT demo routes decide whether the request is public or protected.</small></div>
<article class="card"> <div class="step"><strong>Controller</strong><small>Spring binds params, body, headers, and cookies into method arguments.</small></div>
<h2>AOP Showcase</h2> <div class="step"><strong>Service</strong><small>Business rules run, such as duplicate email checks or stats calculation.</small></div>
<p>Review logging, performance tracing, and rate limiting demonstrations built with Spring AOP.</p> <div class="step"><strong>AOP / Events</strong><small>Cross-cutting timing or event listeners react without cluttering core logic.</small></div>
<code>/aop.html</code> <div class="step"><strong>Response</strong><small>Structured JSON or HTML returns to the page and becomes visible feedback.</small></div>
</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> </div>
</article>
</section> </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> </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> </body>
</html> </html>