From 09574c3400043bb43cee0278845b967a536cd283 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 19 Mar 2026 13:53:49 +0800 Subject: [PATCH] feat: add guided learning cockpit --- .../example/demo/aop/PerformanceAspect.java | 51 +- .../demo/controller/AopEventController.java | 175 +++---- .../demo/controller/LearnController.java | 90 ++-- .../example/demo/event/UserEventListener.java | 60 +-- .../demo/event/UserEventPublisher.java | 51 +- src/main/resources/static/aop.html | 415 ++++++++-------- src/main/resources/static/events.html | 468 +++++++++--------- src/main/resources/static/index.html | 457 +++++++++++++---- 8 files changed, 967 insertions(+), 800 deletions(-) diff --git a/src/main/java/com/example/demo/aop/PerformanceAspect.java b/src/main/java/com/example/demo/aop/PerformanceAspect.java index 5681098..470b296 100644 --- a/src/main/java/com/example/demo/aop/PerformanceAspect.java +++ b/src/main/java/com/example/demo/aop/PerformanceAspect.java @@ -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 totalTimes = new ConcurrentHashMap<>(); private final Map 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> getStatistics() { Map> stats = new HashMap<>(); - for (String method : totalTimes.keySet()) { Map 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; } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/demo/controller/AopEventController.java b/src/main/java/com/example/demo/controller/AopEventController.java index 18232b6..bb30082 100644 --- a/src/main/java/com/example/demo/controller/AopEventController.java +++ b/src/main/java/com/example/demo/controller/AopEventController.java @@ -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 index() { Map 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 aopInfo() { Map 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 getPerformanceStats() { Map 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 eventInfo() { Map 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 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 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 getEventHistory() { + List> items = userEventListener.getEventHistory().stream() + .map(event -> Map.of( + "type", event.getType().name(), + "userId", event.getUserId(), + "userName", event.getUserName(), + "detail", event.getDetail(), + "timestamp", event.getTimestamp().toString() + )) + .toList(); + Map 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 testRateLimit() { Map 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; } -} \ No newline at end of file + + private UserEvent.Type resolveType(String rawType) { + try { + return UserEvent.Type.valueOf(rawType.trim().toUpperCase()); + } catch (Exception ignored) { + return UserEvent.Type.LOGIN; + } + } +} diff --git a/src/main/java/com/example/demo/controller/LearnController.java b/src/main/java/com/example/demo/controller/LearnController.java index 6406c8d..517e91e 100644 --- a/src/main/java/com/example/demo/controller/LearnController.java +++ b/src/main/java/com/example/demo/controller/LearnController.java @@ -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 root() { Map 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 info() { Map 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 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 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 body(@RequestBody Map data) { Map 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 path(@PathVariable String id) { Map 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 header(@RequestHeader(value = "User-Agent", required = false) String userAgent) { Map 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 cookie(@CookieValue(value = "JSESSIONID", required = false) String sessionId) { Map 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 handleException(RuntimeException e) { Map 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; } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/demo/event/UserEventListener.java b/src/main/java/com/example/demo/event/UserEventListener.java index b121c06..e81b3cc 100644 --- a/src/main/java/com/example/demo/event/UserEventListener.java +++ b/src/main/java/com/example/demo/event/UserEventListener.java @@ -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 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 getEventHistory() { return new ArrayList<>(eventHistory); } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/demo/event/UserEventPublisher.java b/src/main/java/com/example/demo/event/UserEventPublisher.java index fc13ef9..93fbcbb 100644 --- a/src/main/java/com/example/demo/event/UserEventPublisher.java +++ b/src/main/java/com/example/demo/event/UserEventPublisher.java @@ -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"); } -} \ No newline at end of file +} diff --git a/src/main/resources/static/aop.html b/src/main/resources/static/aop.html index 89167ea..beefbe3 100644 --- a/src/main/resources/static/aop.html +++ b/src/main/resources/static/aop.html @@ -1,222 +1,229 @@ - + - AOP 切面编程 - Spring Boot + Spring AOP Lab - - -

🔪 AOP 切面编程

- -
-

🧪 实验任务卡(AOP)

- -
    -
  • 目标:观察同一请求如何触发 Before/After/Around 通知
  • -
  • 步骤1:调用用户接口 /api/users
  • -
  • 步骤2:回到本页点击“刷新统计数据”
  • -
  • 预期:统计里能看到 Controller/Service 方法耗时累积
  • -
  • 常见坑:只看页面不看控制台,容易错过切面日志
  • -
-
- -
-

📊 实时性能统计

-

AOP 自动统计所有 Controller 和 Service 方法的执行时间

- - -
点击按钮查看...
-
- -

📚 AOP 核心概念

- -
-

1. 什么是 AOP?

-

AOP (Aspect-Oriented Programming) 面向切面编程,是将横切关注点业务逻辑分离的编程范式。

-
- 横切关注点:日志、事务、安全、性能监控等,散布在多个模块中的公共功能。 +
+
+
AOP Lab
+

Make cross-cutting behavior visible instead of abstract.

+

+ 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. +

+ +
+ +
+ + +
+
Live Runs
+

Run requests and inspect collected metrics

+

The buttons below help you create real traffic, then inspect the aspect output without switching tools.

+ +
+ + + + +
+ +
+
+ What to observe in the console +

Look for controller and service timing lines. Compare success and failure paths to see how around advice still records method duration.

+
+
+ What to inspect in code +

Read PerformanceAspect first, then compare it with the user endpoints that it wraps.

+
+
+ +
Run one of the experiments above to inspect live JSON output.
+
- -
-

2. 核心术语

- - - - - - - -
术语说明
Aspect (切面)横切关注点的模块化封装
JoinPoint (连接点)程序执行的某个点(方法调用、异常抛出等)
Pointcut (切入点)匹配连接点的表达式
Advice (通知)在连接点执行的动作
Weaving (织入)将切面应用到目标对象的过程
-
- -
-

3. 五种通知类型

- - - - - - - -
注解执行时机用途
@Before方法执行前参数校验、权限检查
@After方法执行后(无论成功或异常)资源清理
@AfterReturning方法成功返回后结果处理、日志记录
@AfterThrowing方法抛出异常后异常处理、错误日志
@Around环绕方法执行(最强大)性能统计、事务管理
-
- -

💻 代码示例

- -
-

日志切面示例

-
@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);
-    }
-}
-
- -
-

性能监控切面 (@Around)

-
@Aspect
-@Component
-public class PerformanceAspect {
-    
-    @Around("execution(* com.example.demo..*.*(..))")
-    public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
-        long start = System.currentTimeMillis();
-        
+
+ + + async function sendInvalidUser() { + await renderRequest('/api/users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: '', email: 'bad', age: 999 }) + }); + } + - \ No newline at end of file + diff --git a/src/main/resources/static/events.html b/src/main/resources/static/events.html index 60f8858..16bdcad 100644 --- a/src/main/resources/static/events.html +++ b/src/main/resources/static/events.html @@ -1,253 +1,245 @@ - + - 事件机制 - Spring Boot + Spring Event Lab - - -

📡 Spring 事件机制

- -
-

🧪 实验任务卡(事件)

- -
    -
  • 目标:体验发布者与监听者解耦
  • -
  • 步骤1:输入 userId/userName,点击“发布登录事件”
  • -
  • 步骤2:重复发布不同用户,比较返回结果
  • -
  • 预期:接口立即返回;监听处理在日志中可观察
  • -
  • 常见坑:把事件当同步 RPC,忽略异步监听特性
  • -
-
- -
-

🎉 事件发布演示

-

模拟用户登录事件,观察事件发布和监听过程

-
- - - - +
+
+
Event Lab
+

Understand publisher-listener decoupling through a visible event timeline.

+

+ 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. +

+ -
等待事件发布...
-
- -

🔄 事件机制流程

- -
- 发布者 - - ApplicationEventPublisher - - 事件 - - 监听者 -
- -
- 核心优势: -
    -
  • 解耦:发布者和监听者互不依赖
  • -
  • 扩展:新增监听器无需修改发布者
  • -
  • 异步:耗时操作不阻塞主流程
  • -
-
- -

💻 代码实现

- -
-

1. 定义事件

-
public class UserEvent {
-    public enum Type { CREATED, UPDATED, DELETED, LOGIN }
-    
-    private Type type;
-    private Long userId;
-    private String userName;
-    private LocalDateTime timestamp;
-    
-    // constructor, getters...
-}
-
- -
-

2. 发布事件

-
@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);
-    }
-}
-
- -
-

3. 监听事件

-
@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) {
-        // 发送邮件...
-    }
-}
-
- -
-

4. 控制器中使用

-
@RestController
-@RequestMapping("/aop")
-public class AopEventController {
-    
-    @Autowired
-    private UserEventPublisher eventPublisher;
-    
-    @PostMapping("/event/publish")
-    public Map<String, Object> publishEvent(
-            @RequestParam Long userId,
-            @RequestParam String userName) {
-        
-        // 发布事件
-        eventPublisher.publishUserLogin(userId, userName);
-        
-        return Map.of(
-            "message", "事件已发布",
-            "userId", userId,
-            "userName", userName
-        );
-    }
-}
-
- -

🎯 应用场景

- -
- - - - - - -
场景事件类型处理逻辑
用户注册UserCreatedEvent发送欢迎邮件、初始化数据
订单创建OrderCreatedEvent扣库存、发送通知
支付成功PaymentSuccessEvent更新订单状态、发送短信
用户登录UserLoginEvent记录登录日志、更新在线状态
-
- -
-

💡 最佳实践

-
    -
  • 事件类应该是不可变的(只读属性)
  • -
  • 使用 @Async 处理耗时操作
  • -
  • 避免在监听器中抛出异常
  • -
  • 使用 condition 过滤不需要的事件
  • -
  • 复杂场景考虑使用消息队列(RabbitMQ/Kafka)
  • -
-
- -

← 返回学习中心

- - + async function loadInfo() { + await renderRequest('/aop/event'); + } + - \ No newline at end of file + diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index 0211cf3..39b4888 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -3,159 +3,406 @@ - Spring Boot Learning Hub + Spring Boot Learning Cockpit
- Spring Boot practice -

Explore web, validation, security, and AOP from one clean hub.

-

- 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. -

-
- Open user management - View auth demo - Check health +
+
+
Spring Boot Learning Cockpit
+

Use one workspace to understand MVC, validation, security, AOP, and application events.

+

+ 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. +

+ +
+
Core labs4
+
Major layers6
+
Interactive pages4
+
Tested backend paths2+
+
+
+
+
+
Start Here
+

Recommended study order

+
+
+ 1. Learn endpoint binding +

Hit the live API explorer below and compare query, path, header, cookie, and JSON body patterns.

+
+
+ 2. Study users and validation +

Open the user lab to inspect CRUD, duplicate email handling, and aggregate stats.

+
+
+ 3. Watch cross-cutting behavior +

Move to AOP and events to see timing, rate limiting, and listener history.

+
+
+
+
-
-
-

User Management

-

CRUD, validation, duplicate email protection, search, and stats cards in one interactive page.

- /users.html -
-
-

AOP Showcase

-

Review logging, performance tracing, and rate limiting demonstrations built with Spring AOP.

- /aop.html -
-
-

Event Flow

-

See how controller actions can publish events and decouple downstream reactions.

- /events.html -
- +
+
Architecture
+

How one request travels through the demo

+
+
BrowserForm submit or fetch call starts the request.
+
SecurityJWT demo routes decide whether the request is public or protected.
+
ControllerSpring binds params, body, headers, and cookies into method arguments.
+
ServiceBusiness rules run, such as duplicate email checks or stats calculation.
+
AOP / EventsCross-cutting timing or event listeners react without cluttering core logic.
+
ResponseStructured JSON or HTML returns to the page and becomes visible feedback.
+
+ +
+ + +
+
+
Live Explorer
+

Call real endpoints without leaving the page

+

Use these controls to inspect real JSON responses while you read the code. This makes the project easier to connect to concrete behavior.

+ +
+ + + + + +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+ +
+
Endpoint output
+
Select an experiment above to load live output.
+
+
+ +
+
Experiment 1
+

Trace validation

+

Open the user lab, create a user, then repeat with the same email. Compare the frontend error with `DuplicateEmailException` and the global exception handler.

+
+ UserController + UserService + GlobalExceptionHandler +
+
+ +
+
Experiment 2
+

Trace AOP timing

+

Load users and stats several times, then call `/aop/aop/stats`. Watch how controller and service methods accumulate timing and call count data.

+
+ PerformanceAspect + @Around + Cross-cutting +
+
+ +
+
Experiment 3
+

Trace event decoupling

+

Publish CREATED and LOGIN events, then reload event history. This shows how the request can finish while listeners keep handling side effects.

+
+ Publisher + Listener + @Async +
+
+
+
+ +