feat: add guided learning cockpit

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

View File

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

View File

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

View File

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

View File

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

View File

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