feat: Spring Boot示例项目
- 基础Spring Boot配置 - Docker支持
This commit is contained in:
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
FROM eclipse-temurin:17-jdk-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
RUN apk add --no-cache maven
|
||||||
|
COPY pom.xml .
|
||||||
|
COPY src ./src
|
||||||
|
RUN mvn clean package -DskipTests
|
||||||
|
EXPOSE 8082
|
||||||
|
CMD ["java", "-jar", "target/demo-0.0.1-SNAPSHOT.jar"]
|
||||||
57
pom.xml
Normal file
57
pom.xml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||||
|
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.2.0</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>com.example</groupId>
|
||||||
|
<artifactId>demo</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<name>springboot-demo</name>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>17</java.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Web -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- AOP 切面编程 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-aop</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Actuator 监控 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 测试 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
38
springboot.log
Normal file
38
springboot.log
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
|
||||||
|
. ____ _ __ _ _
|
||||||
|
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
||||||
|
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
||||||
|
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
||||||
|
' |____| .__|_| |_|_| |_\__, | / / / /
|
||||||
|
=========|_|==============|___/=/_/_/_/
|
||||||
|
:: Spring Boot :: (v3.2.0)
|
||||||
|
|
||||||
|
2026-03-06T17:09:02.230Z INFO 1154290 --- [springboot-demo] [ main] com.example.demo.DemoApplication : Starting DemoApplication v0.0.1-SNAPSHOT using Java 21.0.10 with PID 1154290 (/home/llm/projects/springboot-demo/target/demo-0.0.1-SNAPSHOT.jar started by llm in /home/llm/projects/springboot-demo)
|
||||||
|
2026-03-06T17:09:02.380Z INFO 1154290 --- [springboot-demo] [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default"
|
||||||
|
2026-03-06T17:10:05.868Z INFO 1154290 --- [springboot-demo] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8082 (http)
|
||||||
|
2026-03-06T17:10:06.093Z INFO 1154290 --- [springboot-demo] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
|
||||||
|
2026-03-06T17:10:06.093Z INFO 1154290 --- [springboot-demo] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.16]
|
||||||
|
2026-03-06T17:10:07.996Z INFO 1154290 --- [springb2026-03-06T17:10:12.808Z INFO 1153691 --- [springboot-demo] [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint(s) beneath base path '/actuator'
|
||||||
|
2026-03-06T17:10:14.838Z INFO 1153691 --- [springboot-demo] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat start2026-03-06T17:10:23.314Z INFO 1154290 --- [springboot-demo] [ main] o.s.b.a.w.s.WelcomePageHandlerMapping : Adding welcome page: class path resource [static/index.html]
|
||||||
|
2026-03-06T17:10:40.414Z INFO 1154290 --- [springboot-demo] [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint(s) beneath base path '/actuator'
|
||||||
|
2026-03-06T17:10:41.395Z INFO 1154290 --- [springboot-demo] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8082 (http) with context path ''
|
||||||
|
2026-03-06T17:10:41.541Z INFO 1154290 --- [springboot-demo] [ main] com.example.demo.DemoApplication : Started DemoApplication in 116.974 seconds (process running for 133.101)
|
||||||
|
[EventListener] Spring Boot 应用启动完成!
|
||||||
|
2026-03-06T17:11:05.601Z INFO 1154290 --- [springboot-demo] [nio-8082-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
|
||||||
|
2026-03-06T17:11:05.601Z INFO 1154290 --- [springboot-demo] [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
|
||||||
|
2026-03-06T17:11:05.619Z INFO 1154290 --- [springboot-demo] [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 13 ms
|
||||||
|
[AOP-Before] Controller 方法开始: root
|
||||||
|
参数: []
|
||||||
|
[AOP-AfterReturning] 方法返回: root
|
||||||
|
返回值: {aop=https://spring.xiaoxiaoluohao.indevs.in/aop, learn=https://spring.xiaoxiaoluohao.indevs.in/learn, api=https://spring.xiaoxiaoluohao.indevs.in/api/users, message=欢迎来到 Spring Boot 学习脚手架!}
|
||||||
|
[AOP-Performance] LearnController.root() 执行耗时: 18ms
|
||||||
|
[AOP-Before] Controller 方法开始: root
|
||||||
|
参数: []
|
||||||
|
[AOP-AfterReturning] 方法返回: root
|
||||||
|
返回值: {aop=https://spring.xiaoxiaoluohao.indevs.in/aop, learn=https://spring.xiaoxiaoluohao.indevs.in/learn, api=https://spring.xiaoxiaoluohao.indevs.in/api/users, message=欢迎来到 Spring Boot 学习脚手架!}
|
||||||
|
[AOP-Performance] LearnController.root() 执行耗时: 0ms
|
||||||
|
[AOP-Before] Controller 方法开始: root
|
||||||
|
参数: []
|
||||||
|
[AOP-AfterReturning] 方法返回: root
|
||||||
|
返回值: {aop=https://spring.xiaoxiaoluohao.indevs.in/aop, learn=https://spring.xiaoxiaoluohao.indevs.in/learn, api=https://spring.xiaoxiaoluohao.indevs.in/api/users, message=欢迎来到 Spring Boot 学习脚手架!}
|
||||||
|
[AOP-Performance] LearnController.root() 执行耗时: 3ms
|
||||||
23
src/main/java/com/example/demo/DemoApplication.java
Normal file
23
src/main/java/com/example/demo/DemoApplication.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package com.example.demo;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Boot 主应用
|
||||||
|
*
|
||||||
|
* 学习点:
|
||||||
|
* - @SpringBootApplication 组合注解
|
||||||
|
* - @EnableAsync 启用异步处理
|
||||||
|
* - @EnableScheduling 启用定时任务
|
||||||
|
*/
|
||||||
|
@SpringBootApplication
|
||||||
|
@EnableAsync
|
||||||
|
@EnableScheduling
|
||||||
|
public class DemoApplication {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(DemoApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/main/java/com/example/demo/aop/LoggingAspect.java
Normal file
75
src/main/java/com/example/demo/aop/LoggingAspect.java
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package com.example.demo.aop;
|
||||||
|
|
||||||
|
import org.aspectj.lang.JoinPoint;
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.*;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志切面 - 演示 AOP 基础
|
||||||
|
*
|
||||||
|
* 学习点:
|
||||||
|
* - @Aspect 定义切面
|
||||||
|
* - @Pointcut 切入点表达式
|
||||||
|
* - @Before / @After / @Around 通知类型
|
||||||
|
* - JoinPoint 获取方法信息
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class LoggingAspect {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切入点:所有 Controller 方法
|
||||||
|
*/
|
||||||
|
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
|
||||||
|
public void controllerMethods() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切入点:所有 Service 方法
|
||||||
|
*/
|
||||||
|
@Pointcut("execution(* com.example.demo.service.*.*(..))")
|
||||||
|
public void serviceMethods() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 前置通知 - 方法执行前
|
||||||
|
*/
|
||||||
|
@Before("controllerMethods()")
|
||||||
|
public void logBeforeController(JoinPoint joinPoint) {
|
||||||
|
System.out.println("[AOP-Before] Controller 方法开始: " +
|
||||||
|
joinPoint.getSignature().getName());
|
||||||
|
System.out.println(" 参数: " + Arrays.toString(joinPoint.getArgs()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后置通知 - 方法执行后(无论成功或异常)
|
||||||
|
*/
|
||||||
|
@After("serviceMethods()")
|
||||||
|
public void logAfterService(JoinPoint joinPoint) {
|
||||||
|
System.out.println("[AOP-After] Service 方法结束: " +
|
||||||
|
joinPoint.getSignature().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回通知 - 方法成功返回后
|
||||||
|
*/
|
||||||
|
@AfterReturning(pointcut = "controllerMethods()", returning = "result")
|
||||||
|
public void logAfterReturning(JoinPoint joinPoint, Object result) {
|
||||||
|
System.out.println("[AOP-AfterReturning] 方法返回: " +
|
||||||
|
joinPoint.getSignature().getName());
|
||||||
|
System.out.println(" 返回值: " + result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异常通知 - 方法抛出异常后
|
||||||
|
*/
|
||||||
|
@AfterThrowing(pointcut = "controllerMethods() || serviceMethods()", throwing = "error")
|
||||||
|
public void logAfterThrowing(JoinPoint joinPoint, Throwable error) {
|
||||||
|
System.out.println("[AOP-AfterThrowing] 方法异常: " +
|
||||||
|
joinPoint.getSignature().getName());
|
||||||
|
System.out.println(" 异常: " + error.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/main/java/com/example/demo/aop/PerformanceAspect.java
Normal file
77
src/main/java/com/example/demo/aop/PerformanceAspect.java
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package com.example.demo.aop;
|
||||||
|
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Around;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
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.*.*(..))")
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 记录统计
|
||||||
|
totalTimes.merge(methodName, duration, Long::sum);
|
||||||
|
callCounts.merge(methodName, 1L, Long::sum);
|
||||||
|
|
||||||
|
System.out.println("[AOP-Performance] " + methodName +
|
||||||
|
" 执行耗时: " + duration + "ms");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
System.out.println("[AOP-Performance] " + methodName +
|
||||||
|
" 异常耗时: " + (endTime - startTime) + "ms");
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取性能统计
|
||||||
|
*/
|
||||||
|
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));
|
||||||
|
methodStats.put("callCount", callCounts.get(method));
|
||||||
|
methodStats.put("avgTime", totalTimes.get(method) / callCounts.get(method));
|
||||||
|
stats.put(method, methodStats);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/main/java/com/example/demo/aop/RateLimitAspect.java
Normal file
70
src/main/java/com/example/demo/aop/RateLimitAspect.java
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package com.example.demo.aop;
|
||||||
|
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Around;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 限流切面 - 演示 AOP 实现横切关注点
|
||||||
|
*
|
||||||
|
* 学习点:
|
||||||
|
* - @Order 控制切面执行顺序
|
||||||
|
* - AOP 实现非功能性需求
|
||||||
|
* - 横切关注点与业务逻辑分离
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
@Order(1) // 数字越小优先级越高
|
||||||
|
public class RateLimitAspect {
|
||||||
|
|
||||||
|
// 简单的限流计数器(实际项目用 Redis + Token Bucket)
|
||||||
|
private final Map<String, AtomicLong> requestCounts = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Long> lastResetTime = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// 每分钟最大请求数
|
||||||
|
private static final long MAX_REQUESTS_PER_MINUTE = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 限流检查
|
||||||
|
*/
|
||||||
|
@Around("@annotation(com.example.demo.aop.RateLimited)")
|
||||||
|
public Object enforceRateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
|
String methodName = joinPoint.getSignature().toShortString();
|
||||||
|
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
Long lastReset = lastResetTime.get(methodName);
|
||||||
|
|
||||||
|
// 每分钟重置计数器
|
||||||
|
if (lastReset == null || currentTime - lastReset > 60000) {
|
||||||
|
requestCounts.put(methodName, new AtomicLong(0));
|
||||||
|
lastResetTime.put(methodName, currentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
AtomicLong count = requestCounts.get(methodName);
|
||||||
|
long currentCount = count.incrementAndGet();
|
||||||
|
|
||||||
|
if (currentCount > MAX_REQUESTS_PER_MINUTE) {
|
||||||
|
throw new RuntimeException("请求过于频繁,请稍后再试 (Rate Limit)");
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("[AOP-RateLimit] " + methodName +
|
||||||
|
" 当前计数: " + currentCount + "/" + MAX_REQUESTS_PER_MINUTE);
|
||||||
|
|
||||||
|
return joinPoint.proceed();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取限流状态
|
||||||
|
*/
|
||||||
|
public Map<String, Long> getRateLimitStatus() {
|
||||||
|
Map<String, Long> status = new ConcurrentHashMap<>();
|
||||||
|
requestCounts.forEach((k, v) -> status.put(k, v.get()));
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/main/java/com/example/demo/aop/RateLimited.java
Normal file
28
src/main/java/com/example/demo/aop/RateLimited.java
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package com.example.demo.aop;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 限流注解 - 自定义注解配合 AOP 使用
|
||||||
|
*
|
||||||
|
* 学习点:
|
||||||
|
* - 自定义注解定义
|
||||||
|
* - @Retention 保留策略
|
||||||
|
* - @Target 目标元素
|
||||||
|
* - 注解 + AOP 实现声明式功能
|
||||||
|
*/
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Documented
|
||||||
|
public @interface RateLimited {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每分钟最大请求数
|
||||||
|
*/
|
||||||
|
long value() default 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 限流提示消息
|
||||||
|
*/
|
||||||
|
String message() default "请求过于频繁,请稍后再试";
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
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.UserEventPublisher;
|
||||||
|
import com.example.demo.model.User;
|
||||||
|
import com.example.demo.service.UserService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AOP 和事件机制学习控制器
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/aop")
|
||||||
|
public class AopEventController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PerformanceAspect performanceAspect;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RateLimitAspect rateLimitAspect;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 学习首页
|
||||||
|
*/
|
||||||
|
@GetMapping
|
||||||
|
public Map<String, Object> index() {
|
||||||
|
Map<String, Object> info = new HashMap<>();
|
||||||
|
info.put("message", "Spring Boot 学习中心");
|
||||||
|
info.put("topics", new String[]{
|
||||||
|
"AOP 切面编程",
|
||||||
|
"事件机制",
|
||||||
|
"Bean 生命周期"
|
||||||
|
});
|
||||||
|
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 - 限流测试"
|
||||||
|
});
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== AOP 示例 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AOP 概念说明
|
||||||
|
*/
|
||||||
|
@GetMapping("/aop")
|
||||||
|
public Map<String, Object> aopInfo() {
|
||||||
|
Map<String, Object> info = new HashMap<>();
|
||||||
|
info.put("title", "AOP 切面编程");
|
||||||
|
info.put("concepts", new String[]{
|
||||||
|
"Aspect(切面): 横切关注点的模块化",
|
||||||
|
"JoinPoint(连接点): 程序执行的某个点",
|
||||||
|
"Pointcut(切入点): 匹配连接点的表达式",
|
||||||
|
"Advice(通知): 在连接点执行的动作",
|
||||||
|
"Weaving(织入): 将切面应用到目标对象"
|
||||||
|
});
|
||||||
|
info.put("adviceTypes", new String[]{
|
||||||
|
"@Before - 方法执行前",
|
||||||
|
"@After - 方法执行后(无论成功或异常)",
|
||||||
|
"@AfterReturning - 方法成功返回后",
|
||||||
|
"@AfterThrowing - 方法抛出异常后",
|
||||||
|
"@Around - 环绕通知(最强大)"
|
||||||
|
});
|
||||||
|
info.put("useCases", new String[]{
|
||||||
|
"日志记录",
|
||||||
|
"性能监控",
|
||||||
|
"事务管理",
|
||||||
|
"权限检查",
|
||||||
|
"限流控制"
|
||||||
|
});
|
||||||
|
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("statistics", performanceAspect.getStatistics());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 事件机制示例 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件机制说明
|
||||||
|
*/
|
||||||
|
@GetMapping("/event")
|
||||||
|
public Map<String, Object> eventInfo() {
|
||||||
|
Map<String, Object> info = new HashMap<>();
|
||||||
|
info.put("title", "Spring 事件机制");
|
||||||
|
info.put("concepts", new String[]{
|
||||||
|
"ApplicationEventPublisher - 事件发布者",
|
||||||
|
"@EventListener - 事件监听器",
|
||||||
|
"@Async - 异步处理事件",
|
||||||
|
"condition - 条件过滤"
|
||||||
|
});
|
||||||
|
info.put("benefits", new String[]{
|
||||||
|
"解耦:发布者和监听者互不依赖",
|
||||||
|
"扩展:新增监听器无需修改发布者",
|
||||||
|
"异步:耗时操作不阻塞主流程",
|
||||||
|
"测试:更容易进行单元测试"
|
||||||
|
});
|
||||||
|
info.put("eventTypes", new String[]{
|
||||||
|
"CREATED - 用户创建",
|
||||||
|
"UPDATED - 用户更新",
|
||||||
|
"DELETED - 用户删除",
|
||||||
|
"LOGIN - 用户登录"
|
||||||
|
});
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布用户事件
|
||||||
|
*/
|
||||||
|
@PostMapping("/event/publish")
|
||||||
|
public Map<String, Object> publishEvent(
|
||||||
|
@RequestParam Long userId,
|
||||||
|
@RequestParam String userName,
|
||||||
|
@RequestParam(defaultValue = "LOGIN") String eventType
|
||||||
|
) {
|
||||||
|
eventPublisher.publishUserLogin(userId, userName);
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("message", "事件已发布");
|
||||||
|
result.put("eventType", eventType);
|
||||||
|
result.put("userId", userId);
|
||||||
|
result.put("userName", userName);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看事件历史
|
||||||
|
*/
|
||||||
|
@GetMapping("/event/history")
|
||||||
|
public Map<String, Object> getEventHistory() {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("title", "事件历史记录");
|
||||||
|
result.put("note", "由 UserEventListener 自动记录");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 限流示例 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 限流测试接口
|
||||||
|
*/
|
||||||
|
@GetMapping("/ratelimit")
|
||||||
|
@RateLimited(value = 10, message = "测试限流:每分钟最多10次")
|
||||||
|
public Map<String, Object> testRateLimit() {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("message", "请求成功");
|
||||||
|
result.put("note", "使用 @RateLimited 注解");
|
||||||
|
result.put("rateLimitStatus", rateLimitAspect.getRateLimitStatus());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
116
src/main/java/com/example/demo/controller/LearnController.java
Normal file
116
src/main/java/com/example/demo/controller/LearnController.java
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 学习示例控制器
|
||||||
|
*
|
||||||
|
* 学习点:
|
||||||
|
* - 各种参数接收方式
|
||||||
|
* - 配置注入
|
||||||
|
* - 响应格式
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
public class LearnController {
|
||||||
|
|
||||||
|
// 从配置文件注入值
|
||||||
|
@Value("${spring.application.name:demo}")
|
||||||
|
private String appName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根路径 - 重定向到学习中心
|
||||||
|
*/
|
||||||
|
@GetMapping("/")
|
||||||
|
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");
|
||||||
|
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("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 示例"
|
||||||
|
});
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("name", name);
|
||||||
|
result.put("age", age);
|
||||||
|
result.put("tip", "使用 @RequestParam 接收查询参数");
|
||||||
|
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("received", data);
|
||||||
|
result.put("tip", "使用 @RequestBody 接收 JSON 请求体");
|
||||||
|
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("id", id);
|
||||||
|
result.put("tip", "使用 @PathVariable 接收路径变量");
|
||||||
|
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("userAgent", userAgent);
|
||||||
|
result.put("tip", "使用 @RequestHeader 获取请求头");
|
||||||
|
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("sessionId", sessionId);
|
||||||
|
result.put("tip", "使用 @CookieValue 获取 Cookie");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /learn/exception - 异常处理
|
||||||
|
@GetMapping("/learn/exception")
|
||||||
|
public String exception() {
|
||||||
|
throw new RuntimeException("这是一个测试异常");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局异常处理
|
||||||
|
@ExceptionHandler(RuntimeException.class)
|
||||||
|
public Map<String, Object> handleException(RuntimeException e) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("error", e.getMessage());
|
||||||
|
result.put("tip", "使用 @ExceptionHandler 处理异常");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面控制器 - 返回 HTML 页面
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
public class PageController {
|
||||||
|
|
||||||
|
@GetMapping("/home")
|
||||||
|
public String home() {
|
||||||
|
return "forward:/index.html";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import com.example.demo.model.User;
|
||||||
|
import com.example.demo.service.UserService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户控制器 - RESTful API 示例
|
||||||
|
*
|
||||||
|
* 学习点:
|
||||||
|
* - @RestController: 组合了 @Controller 和 @ResponseBody
|
||||||
|
* - @RequestMapping: 路由映射
|
||||||
|
* - @PathVariable: 路径变量
|
||||||
|
* - @RequestParam: 查询参数
|
||||||
|
* - @RequestBody: 请求体
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/users")
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
// GET /api/users - 获取所有用户
|
||||||
|
@GetMapping
|
||||||
|
public List<User> getAllUsers() {
|
||||||
|
return userService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/users/{id} - 获取单个用户
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public User getUserById(@PathVariable Long id) {
|
||||||
|
return userService.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/users - 创建用户
|
||||||
|
@PostMapping
|
||||||
|
public User createUser(@RequestBody User user) {
|
||||||
|
return userService.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /api/users/{id} - 更新用户
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public User updateUser(@PathVariable Long id, @RequestBody User user) {
|
||||||
|
user.setId(id);
|
||||||
|
return userService.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/users/{id} - 删除用户
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public String deleteUser(@PathVariable Long id) {
|
||||||
|
userService.delete(id);
|
||||||
|
return "用户 " + id + " 已删除";
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/users/search?name=xxx - 搜索用户
|
||||||
|
@GetMapping("/search")
|
||||||
|
public List<User> searchUsers(@RequestParam String name) {
|
||||||
|
return userService.findByName(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/main/java/com/example/demo/event/UserEventListener.java
Normal file
80
src/main/java/com/example/demo/event/UserEventListener.java
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package com.example.demo.event;
|
||||||
|
|
||||||
|
import com.example.demo.model.UserEvent;
|
||||||
|
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());
|
||||||
|
|
||||||
|
// 记录到历史
|
||||||
|
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());
|
||||||
|
// 模拟邮件发送耗时
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
System.out.println("[EmailService] 欢迎邮件发送完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听用户登录事件 - 更新登录统计
|
||||||
|
*/
|
||||||
|
@EventListener(condition = "#event.type == T(com.example.demo.model.UserEvent$Type).LOGIN")
|
||||||
|
public void handleUserLogin(UserEvent event) {
|
||||||
|
System.out.println("[LoginTracker] 记录用户登录: " + event.getUserName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听应用启动完成事件
|
||||||
|
*/
|
||||||
|
@EventListener
|
||||||
|
public void onApplicationReady(ApplicationReadyEvent event) {
|
||||||
|
System.out.println("[EventListener] Spring Boot 应用启动完成!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取事件历史
|
||||||
|
*/
|
||||||
|
public List<UserEvent> getEventHistory() {
|
||||||
|
return new ArrayList<>(eventHistory);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/main/java/com/example/demo/event/UserEventPublisher.java
Normal file
55
src/main/java/com/example/demo/event/UserEventPublisher.java
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package com.example.demo.event;
|
||||||
|
|
||||||
|
import com.example.demo.model.UserEvent;
|
||||||
|
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());
|
||||||
|
eventPublisher.publishEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便捷方法:发布用户创建事件
|
||||||
|
*/
|
||||||
|
public void publishUserCreated(Long userId, String userName) {
|
||||||
|
UserEvent event = new UserEvent(
|
||||||
|
UserEvent.Type.CREATED,
|
||||||
|
userId,
|
||||||
|
userName,
|
||||||
|
"新用户注册成功"
|
||||||
|
);
|
||||||
|
publishEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便捷方法:发布用户登录事件
|
||||||
|
*/
|
||||||
|
public void publishUserLogin(Long userId, String userName) {
|
||||||
|
UserEvent event = new UserEvent(
|
||||||
|
UserEvent.Type.LOGIN,
|
||||||
|
userId,
|
||||||
|
userName,
|
||||||
|
"用户登录成功"
|
||||||
|
);
|
||||||
|
publishEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/main/java/com/example/demo/model/User.java
Normal file
34
src/main/java/com/example/demo/model/User.java
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package com.example.demo.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户实体类
|
||||||
|
*
|
||||||
|
* 学习点:
|
||||||
|
* - JavaBean / POJO 设计模式
|
||||||
|
* - Lombok 可以简化 getter/setter(这里用原生写法演示)
|
||||||
|
*/
|
||||||
|
public class User {
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
private String email;
|
||||||
|
private Integer age;
|
||||||
|
|
||||||
|
public User() {}
|
||||||
|
|
||||||
|
public User(Long id, String name, String email, Integer age) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.email = email;
|
||||||
|
this.age = age;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
public Integer getAge() { return age; }
|
||||||
|
public void setAge(Integer age) { this.age = age; }
|
||||||
|
}
|
||||||
51
src/main/java/com/example/demo/model/UserEvent.java
Normal file
51
src/main/java/com/example/demo/model/UserEvent.java
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package com.example.demo.model;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户事件 - 演示 Spring 事件机制
|
||||||
|
*
|
||||||
|
* 学习点:
|
||||||
|
* - ApplicationEvent 基类
|
||||||
|
* - 事件驱动架构
|
||||||
|
* - 观察者模式
|
||||||
|
*/
|
||||||
|
public class UserEvent {
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
CREATED, // 用户创建
|
||||||
|
UPDATED, // 用户更新
|
||||||
|
DELETED, // 用户删除
|
||||||
|
LOGIN // 用户登录
|
||||||
|
}
|
||||||
|
|
||||||
|
private Type type;
|
||||||
|
private Long userId;
|
||||||
|
private String userName;
|
||||||
|
private LocalDateTime timestamp;
|
||||||
|
private String detail;
|
||||||
|
|
||||||
|
public UserEvent() {
|
||||||
|
this.timestamp = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserEvent(Type type, Long userId, String userName, String detail) {
|
||||||
|
this.type = type;
|
||||||
|
this.userId = userId;
|
||||||
|
this.userName = userName;
|
||||||
|
this.detail = detail;
|
||||||
|
this.timestamp = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Type getType() { return type; }
|
||||||
|
public void setType(Type type) { this.type = type; }
|
||||||
|
public Long getUserId() { return userId; }
|
||||||
|
public void setUserId(Long userId) { this.userId = userId; }
|
||||||
|
public String getUserName() { return userName; }
|
||||||
|
public void setUserName(String userName) { this.userName = userName; }
|
||||||
|
public LocalDateTime getTimestamp() { return timestamp; }
|
||||||
|
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
|
||||||
|
public String getDetail() { return detail; }
|
||||||
|
public void setDetail(String detail) { this.detail = detail; }
|
||||||
|
}
|
||||||
69
src/main/java/com/example/demo/service/UserService.java
Normal file
69
src/main/java/com/example/demo/service/UserService.java
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package com.example.demo.service;
|
||||||
|
|
||||||
|
import com.example.demo.model.User;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户服务 - 业务逻辑层
|
||||||
|
*
|
||||||
|
* 学习点:
|
||||||
|
* - @Service: 标记为服务层组件,自动注册为 Bean
|
||||||
|
* - 依赖注入:Controller 通过 @Autowired 注入此服务
|
||||||
|
* - 分层架构:Controller -> Service -> Repository
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class UserService {
|
||||||
|
|
||||||
|
// 内存存储(演示用,实际项目用数据库)
|
||||||
|
private final List<User> users = new ArrayList<>();
|
||||||
|
private final AtomicLong idGenerator = new AtomicLong(1);
|
||||||
|
|
||||||
|
public UserService() {
|
||||||
|
// 初始化一些测试数据
|
||||||
|
users.add(new User(idGenerator.getAndIncrement(), "张三", "zhangsan@example.com", 25));
|
||||||
|
users.add(new User(idGenerator.getAndIncrement(), "李四", "lisi@example.com", 30));
|
||||||
|
users.add(new User(idGenerator.getAndIncrement(), "王五", "wangwu@example.com", 28));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<User> findAll() {
|
||||||
|
return new ArrayList<>(users);
|
||||||
|
}
|
||||||
|
|
||||||
|
public User findById(Long id) {
|
||||||
|
return users.stream()
|
||||||
|
.filter(u -> u.getId().equals(id))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<User> findByName(String name) {
|
||||||
|
return users.stream()
|
||||||
|
.filter(u -> u.getName().contains(name))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public User save(User user) {
|
||||||
|
if (user.getId() == null) {
|
||||||
|
user.setId(idGenerator.getAndIncrement());
|
||||||
|
users.add(user);
|
||||||
|
} else {
|
||||||
|
// 更新
|
||||||
|
for (int i = 0; i < users.size(); i++) {
|
||||||
|
if (users.get(i).getId().equals(user.getId())) {
|
||||||
|
users.set(i, user);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(Long id) {
|
||||||
|
users.removeIf(u -> u.getId().equals(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/main/resources/application.properties
Normal file
2
src/main/resources/application.properties
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
server.port=8082
|
||||||
|
spring.application.name=springboot-demo
|
||||||
177
src/main/resources/static/aop.html
Normal file
177
src/main/resources/static/aop.html
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>AOP 切面编程 - Spring Boot</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
|
||||||
|
h1 { color: #6DB33F; margin: 20px 0; }
|
||||||
|
h2 { color: #333; border-bottom: 2px solid #6DB33F; padding-bottom: 10px; margin: 20px 0 15px; }
|
||||||
|
h3 { color: #6DB33F; margin: 15px 0 10px; }
|
||||||
|
.card { background: white; padding: 25px; margin: 15px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.btn { display: inline-block; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 500; cursor: pointer; border: none; }
|
||||||
|
.btn-primary { background: #6DB33F; color: white; }
|
||||||
|
.btn-primary:hover { background: #5da32f; }
|
||||||
|
.btn-info { background: #17a2b8; color: white; }
|
||||||
|
code { background: #f0f0f0; padding: 2px 8px; border-radius: 4px; font-family: 'Fira Code', monospace; }
|
||||||
|
pre { background: #2d2d2d; color: #f8f8f2; padding: 20px; border-radius: 8px; overflow-x: auto; margin: 15px 0; }
|
||||||
|
pre code { background: none; color: inherit; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
|
||||||
|
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
|
||||||
|
th { background: #6DB33F; color: white; }
|
||||||
|
tr:nth-child(even) { background: #f9f9f9; }
|
||||||
|
.tip { background: #e7f3ff; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #6DB33F; }
|
||||||
|
.warn { background: #fff3cd; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #ffc107; }
|
||||||
|
.result-box { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 8px; font-family: monospace; white-space: pre-wrap; margin-top: 10px; }
|
||||||
|
.nav { margin-bottom: 20px; }
|
||||||
|
.nav a { margin-right: 15px; color: #6DB33F; text-decoration: none; }
|
||||||
|
.nav a:hover { text-decoration: underline; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="nav">
|
||||||
|
<a href="/">← 返回首页</a>
|
||||||
|
<a href="/users.html">用户管理</a>
|
||||||
|
<a href="/events.html">事件机制</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>🔪 AOP 切面编程</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>📊 实时性能统计</h3>
|
||||||
|
<p>AOP 自动统计所有 Controller 和 Service 方法的执行时间</p>
|
||||||
|
<button class="btn btn-primary" onclick="loadStats()">刷新统计数据</button>
|
||||||
|
<div class="result-box" id="statsResult">点击按钮查看...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>📚 AOP 核心概念</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>1. 什么是 AOP?</h3>
|
||||||
|
<p>AOP (Aspect-Oriented Programming) 面向切面编程,是将<strong>横切关注点</strong>与<strong>业务逻辑</strong>分离的编程范式。</p>
|
||||||
|
<div class="tip">
|
||||||
|
<strong>横切关注点:</strong>日志、事务、安全、性能监控等,散布在多个模块中的公共功能。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>2. 核心术语</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>术语</th><th>说明</th></tr>
|
||||||
|
<tr><td><strong>Aspect (切面)</strong></td><td>横切关注点的模块化封装</td></tr>
|
||||||
|
<tr><td><strong>JoinPoint (连接点)</strong></td><td>程序执行的某个点(方法调用、异常抛出等)</td></tr>
|
||||||
|
<tr><td><strong>Pointcut (切入点)</strong></td><td>匹配连接点的表达式</td></tr>
|
||||||
|
<tr><td><strong>Advice (通知)</strong></td><td>在连接点执行的动作</td></tr>
|
||||||
|
<tr><td><strong>Weaving (织入)</strong></td><td>将切面应用到目标对象的过程</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>3. 五种通知类型</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>注解</th><th>执行时机</th><th>用途</th></tr>
|
||||||
|
<tr><td><code>@Before</code></td><td>方法执行前</td><td>参数校验、权限检查</td></tr>
|
||||||
|
<tr><td><code>@After</code></td><td>方法执行后(无论成功或异常)</td><td>资源清理</td></tr>
|
||||||
|
<tr><td><code>@AfterReturning</code></td><td>方法成功返回后</td><td>结果处理、日志记录</td></tr>
|
||||||
|
<tr><td><code>@AfterThrowing</code></td><td>方法抛出异常后</td><td>异常处理、错误日志</td></tr>
|
||||||
|
<tr><td><code>@Around</code></td><td>环绕方法执行(最强大)</td><td>性能统计、事务管理</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>💻 代码示例</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>日志切面示例</h3>
|
||||||
|
<pre><code>@Aspect
|
||||||
|
@Component
|
||||||
|
public class LoggingAspect {
|
||||||
|
|
||||||
|
// 切入点:匹配所有 Controller 方法
|
||||||
|
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
|
||||||
|
public void controllerMethods() {}
|
||||||
|
|
||||||
|
// 前置通知
|
||||||
|
@Before("controllerMethods()")
|
||||||
|
public void logBefore(JoinPoint jp) {
|
||||||
|
System.out.println("[AOP-Before] 方法开始: " + jp.getSignature().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回通知
|
||||||
|
@AfterReturning(pointcut = "controllerMethods()", returning = "result")
|
||||||
|
public void logAfterReturning(JoinPoint jp, Object result) {
|
||||||
|
System.out.println("[AOP-AfterReturning] 返回值: " + result);
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>性能监控切面 (@Around)</h3>
|
||||||
|
<pre><code>@Aspect
|
||||||
|
@Component
|
||||||
|
public class PerformanceAspect {
|
||||||
|
|
||||||
|
@Around("execution(* com.example.demo..*.*(..))")
|
||||||
|
public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Object result = pjp.proceed(); // 执行目标方法
|
||||||
|
long duration = System.currentTimeMillis() - start;
|
||||||
|
System.out.println("[AOP] " + pjp.getSignature() + " 耗时: " + duration + "ms");
|
||||||
|
return result;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
System.out.println("[AOP] 方法异常: " + e.getMessage());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>切入点表达式语法</h3>
|
||||||
|
<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>
|
||||||
|
async function loadStats() {
|
||||||
|
const res = await fetch('/aop/stats');
|
||||||
|
const data = await res.json();
|
||||||
|
document.getElementById('statsResult').textContent = JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
213
src/main/resources/static/events.html
Normal file
213
src/main/resources/static/events.html
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>事件机制 - Spring Boot</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
|
||||||
|
h1 { color: #6DB33F; margin: 20px 0; }
|
||||||
|
h2 { color: #333; border-bottom: 2px solid #6DB33F; padding-bottom: 10px; margin: 20px 0 15px; }
|
||||||
|
h3 { color: #6DB33F; margin: 15px 0 10px; }
|
||||||
|
.card { background: white; padding: 25px; margin: 15px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.btn { display: inline-block; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 500; cursor: pointer; border: none; margin: 5px; }
|
||||||
|
.btn-primary { background: #6DB33F; color: white; }
|
||||||
|
.btn-primary:hover { background: #5da32f; }
|
||||||
|
.btn-info { background: #17a2b8; color: white; }
|
||||||
|
.btn-warning { background: #ffc107; color: #333; }
|
||||||
|
code { background: #f0f0f0; padding: 2px 8px; border-radius: 4px; font-family: 'Fira Code', monospace; }
|
||||||
|
pre { background: #2d2d2d; color: #f8f8f2; padding: 20px; border-radius: 8px; overflow-x: auto; margin: 15px 0; }
|
||||||
|
pre code { background: none; color: inherit; }
|
||||||
|
.tip { background: #e7f3ff; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #6DB33F; }
|
||||||
|
.result-box { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 8px; font-family: monospace; white-space: pre-wrap; margin-top: 10px; max-height: 300px; overflow-y: auto; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
|
||||||
|
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
|
||||||
|
th { background: #6DB33F; color: white; }
|
||||||
|
.event-flow { display: flex; align-items: center; justify-content: center; margin: 20px 0; }
|
||||||
|
.event-flow span { padding: 15px 25px; background: #6DB33F; color: white; border-radius: 8px; margin: 0 10px; }
|
||||||
|
.event-flow .arrow { font-size: 24px; color: #6DB33F; }
|
||||||
|
.nav { margin-bottom: 20px; }
|
||||||
|
.nav a { margin-right: 15px; color: #6DB33F; text-decoration: none; }
|
||||||
|
.nav a:hover { text-decoration: underline; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="nav">
|
||||||
|
<a href="/">← 返回首页</a>
|
||||||
|
<a href="/users.html">用户管理</a>
|
||||||
|
<a href="/aop.html">AOP 切面</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>📡 Spring 事件机制</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>🎉 事件发布演示</h3>
|
||||||
|
<p>模拟用户登录事件,观察事件发布和监听过程</p>
|
||||||
|
<div style="margin: 15px 0;">
|
||||||
|
<input type="text" id="userName" placeholder="用户名" value="张三" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; width: 150px;">
|
||||||
|
<input type="number" id="userId" placeholder="用户ID" value="1" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; width: 100px;">
|
||||||
|
<button class="btn btn-primary" onclick="publishEvent()">发布登录事件</button>
|
||||||
|
</div>
|
||||||
|
<div class="result-box" id="eventResult">等待事件发布...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>🔄 事件机制流程</h2>
|
||||||
|
|
||||||
|
<div class="event-flow">
|
||||||
|
<span>发布者</span>
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<span>ApplicationEventPublisher</span>
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<span>事件</span>
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<span>监听者</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tip">
|
||||||
|
<strong>核心优势:</strong>
|
||||||
|
<ul style="margin-top: 10px; padding-left: 20px;">
|
||||||
|
<li><strong>解耦:</strong>发布者和监听者互不依赖</li>
|
||||||
|
<li><strong>扩展:</strong>新增监听器无需修改发布者</li>
|
||||||
|
<li><strong>异步:</strong>耗时操作不阻塞主流程</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>💻 代码实现</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>1. 定义事件</h3>
|
||||||
|
<pre><code>public class UserEvent {
|
||||||
|
public enum Type { CREATED, UPDATED, DELETED, LOGIN }
|
||||||
|
|
||||||
|
private Type type;
|
||||||
|
private Long userId;
|
||||||
|
private String userName;
|
||||||
|
private LocalDateTime timestamp;
|
||||||
|
|
||||||
|
// constructor, getters...
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>2. 发布事件</h3>
|
||||||
|
<pre><code>@Component
|
||||||
|
public class UserEventPublisher {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
public void publishUserLogin(Long userId, String userName) {
|
||||||
|
UserEvent event = new UserEvent(
|
||||||
|
UserEvent.Type.LOGIN,
|
||||||
|
userId,
|
||||||
|
userName,
|
||||||
|
"用户登录成功"
|
||||||
|
);
|
||||||
|
eventPublisher.publishEvent(event);
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>3. 监听事件</h3>
|
||||||
|
<pre><code>@Component
|
||||||
|
public class UserEventListener {
|
||||||
|
|
||||||
|
// 基础监听
|
||||||
|
@EventListener
|
||||||
|
public void handleUserEvent(UserEvent event) {
|
||||||
|
System.out.println("收到事件: " + event.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 条件监听 - 只处理登录事件
|
||||||
|
@EventListener(condition = "#event.type == T(com.example.demo.model.UserEvent$Type).LOGIN")
|
||||||
|
public void handleLogin(UserEvent event) {
|
||||||
|
System.out.println("用户登录: " + event.getUserName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步监听 - 不阻塞主流程
|
||||||
|
@Async
|
||||||
|
@EventListener
|
||||||
|
public void sendWelcomeEmail(UserEvent event) {
|
||||||
|
// 发送邮件...
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>4. 控制器中使用</h3>
|
||||||
|
<pre><code>@RestController
|
||||||
|
@RequestMapping("/aop")
|
||||||
|
public class AopEventController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
@PostMapping("/event/publish")
|
||||||
|
public Map<String, Object> publishEvent(
|
||||||
|
@RequestParam Long userId,
|
||||||
|
@RequestParam String userName) {
|
||||||
|
|
||||||
|
// 发布事件
|
||||||
|
eventPublisher.publishUserLogin(userId, userName);
|
||||||
|
|
||||||
|
return Map.of(
|
||||||
|
"message", "事件已发布",
|
||||||
|
"userId", userId,
|
||||||
|
"userName", userName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>🎯 应用场景</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<table>
|
||||||
|
<tr><th>场景</th><th>事件类型</th><th>处理逻辑</th></tr>
|
||||||
|
<tr><td>用户注册</td><td>UserCreatedEvent</td><td>发送欢迎邮件、初始化数据</td></tr>
|
||||||
|
<tr><td>订单创建</td><td>OrderCreatedEvent</td><td>扣库存、发送通知</td></tr>
|
||||||
|
<tr><td>支付成功</td><td>PaymentSuccessEvent</td><td>更新订单状态、发送短信</td></tr>
|
||||||
|
<tr><td>用户登录</td><td>UserLoginEvent</td><td>记录登录日志、更新在线状态</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>💡 最佳实践</h3>
|
||||||
|
<ul style="line-height: 2; padding-left: 20px;">
|
||||||
|
<li>事件类应该是<strong>不可变</strong>的(只读属性)</li>
|
||||||
|
<li>使用 <code>@Async</code> 处理耗时操作</li>
|
||||||
|
<li>避免在监听器中抛出异常</li>
|
||||||
|
<li>使用 <code>condition</code> 过滤不需要的事件</li>
|
||||||
|
<li>复杂场景考虑使用消息队列(RabbitMQ/Kafka)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top: 30px;"><a href="/">← 返回学习中心</a></p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function publishEvent() {
|
||||||
|
const userId = document.getElementById('userId').value;
|
||||||
|
const userName = document.getElementById('userName').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/aop/event/publish?userId=${userId}&userName=${encodeURIComponent(userName)}`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
const resultBox = document.getElementById('eventResult');
|
||||||
|
resultBox.textContent = `[${new Date().toLocaleTimeString()}] 事件已发布\n\n` +
|
||||||
|
JSON.stringify(data, null, 2) + '\n\n' +
|
||||||
|
'📊 查看控制台日志可以看到监听器的输出:\n' +
|
||||||
|
'[EventPublisher] 发布事件: LOGIN - ' + userName + '\n' +
|
||||||
|
'[EventListener] 收到事件: LOGIN - 用户: ' + userName + '\n' +
|
||||||
|
'[LoginTracker] 记录用户登录: ' + userName;
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('eventResult').textContent = '错误: ' + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
221
src/main/resources/static/index.html
Normal file
221
src/main/resources/static/index.html
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Spring Boot 学习中心</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
|
||||||
|
h1 { color: #6DB33F; text-align: center; margin: 30px 0; font-size: 2.5em; }
|
||||||
|
h2 { color: #333; border-bottom: 3px solid #6DB33F; padding-bottom: 10px; margin: 20px 0 15px; }
|
||||||
|
.card { background: white; padding: 25px; margin: 15px 0; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
||||||
|
.card h3 { color: #6DB33F; margin-bottom: 15px; font-size: 1.3em; }
|
||||||
|
.btn-group { display: flex; flex-wrap: wrap; gap: 10px; margin: 15px 0; }
|
||||||
|
.btn { display: inline-block; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 500; transition: all 0.3s; }
|
||||||
|
.btn-primary { background: #6DB33F; color: white; }
|
||||||
|
.btn-primary:hover { background: #5da32f; transform: translateY(-2px); }
|
||||||
|
.btn-secondary { background: #333; color: white; }
|
||||||
|
.btn-secondary:hover { background: #444; }
|
||||||
|
.btn-info { background: #17a2b8; color: white; }
|
||||||
|
.btn-info:hover { background: #138496; }
|
||||||
|
.btn-warning { background: #ffc107; color: #333; }
|
||||||
|
.btn-warning:hover { background: #e0a800; }
|
||||||
|
code { background: #f0f0f0; padding: 2px 8px; border-radius: 4px; font-family: 'Fira Code', monospace; font-size: 14px; }
|
||||||
|
pre { background: #2d2d2d; color: #f8f8f2; padding: 20px; border-radius: 8px; overflow-x: auto; margin: 15px 0; }
|
||||||
|
pre code { background: none; color: inherit; }
|
||||||
|
ul { line-height: 2; padding-left: 20px; }
|
||||||
|
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; }
|
||||||
|
.feature-item { background: #f8f9fa; padding: 20px; border-radius: 8px; border-left: 4px solid #6DB33F; transition: all 0.3s; }
|
||||||
|
.feature-item:hover { transform: translateY(-3px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
||||||
|
.feature-item h4 { color: #333; margin-bottom: 8px; font-size: 1.1em; }
|
||||||
|
.feature-item p { color: #666; font-size: 14px; margin: 0; }
|
||||||
|
.feature-item a { color: inherit; text-decoration: none; }
|
||||||
|
.api-test { background: #f8f9fa; padding: 15px; margin: 10px 0; border-radius: 8px; }
|
||||||
|
.api-test input, .api-test select { padding: 10px; border: 1px solid #ddd; border-radius: 4px; margin: 5px; }
|
||||||
|
.api-test button { padding: 10px 20px; background: #6DB33F; color: white; border: none; border-radius: 4px; cursor: pointer; }
|
||||||
|
.api-test button:hover { background: #5da32f; }
|
||||||
|
#result { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 8px; margin-top: 10px; white-space: pre-wrap; font-family: monospace; font-size: 14px; }
|
||||||
|
.footer { text-align: center; margin-top: 40px; padding: 20px; color: #666; border-top: 1px solid #ddd; }
|
||||||
|
.nav-links { display: flex; justify-content: center; gap: 15px; margin-bottom: 30px; }
|
||||||
|
.nav-links a { padding: 10px 20px; background: white; border-radius: 8px; text-decoration: none; color: #6DB33F; font-weight: 500; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.nav-links a:hover { background: #6DB33F; color: white; }
|
||||||
|
.nav-links a.active { background: #6DB33F; color: white; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🍃 Spring Boot 学习中心</h1>
|
||||||
|
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="/" class="active">首页</a>
|
||||||
|
<a href="/users.html">用户管理</a>
|
||||||
|
<a href="/aop.html">AOP 切面</a>
|
||||||
|
<a href="/events.html">事件机制</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>📚 学习模块</h3>
|
||||||
|
<div class="feature-grid">
|
||||||
|
<a href="/users.html" class="feature-item">
|
||||||
|
<h4>👥 用户管理</h4>
|
||||||
|
<p>RESTful API 设计、CRUD 操作、参数绑定</p>
|
||||||
|
</a>
|
||||||
|
<a href="/aop.html" class="feature-item">
|
||||||
|
<h4>🔪 AOP 切面编程</h4>
|
||||||
|
<p>日志记录、性能监控、限流控制</p>
|
||||||
|
</a>
|
||||||
|
<a href="/events.html" class="feature-item">
|
||||||
|
<h4>📡 事件机制</h4>
|
||||||
|
<p>发布/订阅模式、解耦业务逻辑</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>🔗 快速链接</h2>
|
||||||
|
<div class="card">
|
||||||
|
<div class="btn-group">
|
||||||
|
<a class="btn btn-primary" href="/learn">API 接口列表</a>
|
||||||
|
<a class="btn btn-info" href="/api/users">用户 JSON</a>
|
||||||
|
<a class="btn btn-secondary" href="/actuator/health">健康检查</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>🧪 接口测试</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>GET 参数示例</h3>
|
||||||
|
<div class="api-test">
|
||||||
|
<input type="text" id="param-name" placeholder="姓名" value="张三">
|
||||||
|
<input type="number" id="param-age" placeholder="年龄" value="25">
|
||||||
|
<button onclick="testParams()">测试</button>
|
||||||
|
<div id="result-params"></div>
|
||||||
|
</div>
|
||||||
|
<p><code>GET /learn/params?name=xxx&age=18</code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>路径变量示例</h3>
|
||||||
|
<div class="api-test">
|
||||||
|
<input type="text" id="path-id" placeholder="ID" value="123">
|
||||||
|
<button onclick="testPath()">测试</button>
|
||||||
|
<div id="result-path"></div>
|
||||||
|
</div>
|
||||||
|
<p><code>GET /learn/path/{id}</code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>POST JSON 示例</h3>
|
||||||
|
<div class="api-test">
|
||||||
|
<input type="text" id="post-data" placeholder='JSON 数据' value='{"name":"test","value":123}' style="width: 300px;">
|
||||||
|
<button onclick="testPost()">测试</button>
|
||||||
|
<div id="result-post"></div>
|
||||||
|
</div>
|
||||||
|
<p><code>POST /learn/body</code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>📖 学习路径</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>1. IOC 容器</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code>@Component</code>, <code>@Service</code>, <code>@Repository</code>, <code>@Controller</code></li>
|
||||||
|
<li><code>@Autowired</code> 依赖注入</li>
|
||||||
|
<li><code>@Configuration</code> + <code>@Bean</code> 配置类</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>2. Web 开发</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code>@RestController</code> = <code>@Controller</code> + <code>@ResponseBody</code></li>
|
||||||
|
<li><code>@RequestMapping</code>, <code>@GetMapping</code>, <code>@PostMapping</code></li>
|
||||||
|
<li><code>@PathVariable</code>, <code>@RequestParam</code>, <code>@RequestBody</code></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>3. AOP 切面编程</h3>
|
||||||
|
<pre><code>@Aspect
|
||||||
|
@Component
|
||||||
|
public class LoggingAspect {
|
||||||
|
@Before("execution(* com.example.*.*(..))")
|
||||||
|
public void logBefore(JoinPoint jp) {
|
||||||
|
System.out.println("方法调用: " + jp.getSignature());
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>4. 事件机制</h3>
|
||||||
|
<pre><code>// 发布事件
|
||||||
|
@Autowired
|
||||||
|
ApplicationEventPublisher publisher;
|
||||||
|
publisher.publishEvent(new UserEvent(...));
|
||||||
|
|
||||||
|
// 监听事件
|
||||||
|
@EventListener
|
||||||
|
public void onEvent(UserEvent event) {
|
||||||
|
// 处理事件
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>📁 项目结构</h2>
|
||||||
|
<div class="card">
|
||||||
|
<pre><code>├── src/main/java/com/example/demo/
|
||||||
|
│ ├── DemoApplication.java # 启动类
|
||||||
|
│ ├── controller/ # 控制器层
|
||||||
|
│ │ ├── LearnController.java # 学习示例
|
||||||
|
│ │ ├── UserController.java # 用户 API
|
||||||
|
│ │ └── AopEventController.java
|
||||||
|
│ ├── service/ # 业务逻辑层
|
||||||
|
│ ├── model/ # 实体类
|
||||||
|
│ ├── aop/ # AOP 切面
|
||||||
|
│ │ ├── LoggingAspect.java
|
||||||
|
│ │ ├── PerformanceAspect.java
|
||||||
|
│ │ └── RateLimitAspect.java
|
||||||
|
│ └── event/ # 事件机制
|
||||||
|
│ ├── UserEventPublisher.java
|
||||||
|
│ └── UserEventListener.java
|
||||||
|
├── src/main/resources/
|
||||||
|
│ ├── static/ # 静态资源
|
||||||
|
│ │ ├── index.html
|
||||||
|
│ │ ├── users.html
|
||||||
|
│ │ ├── aop.html
|
||||||
|
│ │ └── events.html
|
||||||
|
│ └── application.properties # 配置文件
|
||||||
|
└── pom.xml # Maven 配置</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>🍃 Spring Boot 学习脚手架 | <a href="https://spring.io" style="color: #6DB33F;">Spring 官网</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function testParams() {
|
||||||
|
const name = document.getElementById('param-name').value;
|
||||||
|
const age = document.getElementById('param-age').value;
|
||||||
|
const res = await fetch(`/learn/params?name=${encodeURIComponent(name)}&age=${age}`);
|
||||||
|
const data = await res.json();
|
||||||
|
document.getElementById('result-params').innerHTML = '<div id="result">' + JSON.stringify(data, null, 2) + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testPath() {
|
||||||
|
const id = document.getElementById('path-id').value;
|
||||||
|
const res = await fetch(`/learn/path/${id}`);
|
||||||
|
const data = await res.json();
|
||||||
|
document.getElementById('result-path').innerHTML = '<div id="result">' + JSON.stringify(data, null, 2) + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testPost() {
|
||||||
|
const data = document.getElementById('post-data').value;
|
||||||
|
const res = await fetch('/learn/body', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: data
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
document.getElementById('result-post').innerHTML = '<div id="result">' + JSON.stringify(result, null, 2) + '</div>';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
225
src/main/resources/static/users.html
Normal file
225
src/main/resources/static/users.html
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>用户管理 - Spring Boot</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
|
||||||
|
h1 { color: #6DB33F; margin: 20px 0; }
|
||||||
|
h2 { color: #333; margin: 20px 0 10px; }
|
||||||
|
.card { background: white; padding: 20px; margin: 15px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
|
||||||
|
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
|
||||||
|
th { background: #6DB33F; color: white; }
|
||||||
|
tr:nth-child(even) { background: #f9f9f9; }
|
||||||
|
tr:hover { background: #f0f0f0; }
|
||||||
|
.btn { display: inline-block; padding: 8px 16px; border-radius: 4px; text-decoration: none; font-size: 14px; cursor: pointer; border: none; }
|
||||||
|
.btn-primary { background: #6DB33F; color: white; }
|
||||||
|
.btn-primary:hover { background: #5da32f; }
|
||||||
|
.btn-danger { background: #dc3545; color: white; }
|
||||||
|
.btn-danger:hover { background: #c82333; }
|
||||||
|
.btn-secondary { background: #6c757d; color: white; }
|
||||||
|
.btn-success { background: #28a745; color: white; }
|
||||||
|
.form-group { margin: 15px 0; }
|
||||||
|
.form-group label { display: block; margin-bottom: 5px; font-weight: bold; }
|
||||||
|
.form-group input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
|
||||||
|
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); }
|
||||||
|
.modal.active { display: flex; justify-content: center; align-items: center; }
|
||||||
|
.modal-content { background: white; padding: 30px; border-radius: 8px; width: 400px; }
|
||||||
|
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||||
|
.modal-header h3 { margin: 0; }
|
||||||
|
.close-btn { background: none; border: none; font-size: 24px; cursor: pointer; }
|
||||||
|
.btn-group { display: flex; gap: 10px; margin-top: 20px; }
|
||||||
|
.tip { background: #e7f3ff; padding: 15px; border-radius: 4px; margin: 15px 0; border-left: 4px solid #6DB33F; }
|
||||||
|
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; }
|
||||||
|
pre { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 4px; overflow-x: auto; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>👥 用户管理 - RESTful API 示例</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<h2>用户列表</h2>
|
||||||
|
<button class="btn btn-primary" onclick="openModal()">+ 添加用户</button>
|
||||||
|
</div>
|
||||||
|
<table id="userTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>姓名</th>
|
||||||
|
<th>邮箱</th>
|
||||||
|
<th>年龄</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>📖 学习要点</h2>
|
||||||
|
<div class="tip">
|
||||||
|
<strong>RESTful API 设计:</strong>
|
||||||
|
<ul style="margin-top: 10px; padding-left: 20px;">
|
||||||
|
<li><code>GET /api/users</code> - 获取所有用户</li>
|
||||||
|
<li><code>GET /api/users/{id}</code> - 获取单个用户</li>
|
||||||
|
<li><code>POST /api/users</code> - 创建用户</li>
|
||||||
|
<li><code>PUT /api/users/{id}</code> - 更新用户</li>
|
||||||
|
<li><code>DELETE /api/users/{id}</code> - 删除用户</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Controller 代码示例</h3>
|
||||||
|
<pre><code>@RestController
|
||||||
|
@RequestMapping("/api/users")
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<User> getAllUsers() { ... }
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public User getUserById(@PathVariable Long id) { ... }
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public User createUser(@RequestBody User user) { ... }
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public User updateUser(@PathVariable Long id, @RequestBody User user) { ... }
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public String deleteUser(@PathVariable Long id) { ... }
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>🔧 Spring 注解说明</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th>注解</th><th>说明</th></tr>
|
||||||
|
<tr><td><code>@RestController</code></td><td>= @Controller + @ResponseBody</td></tr>
|
||||||
|
<tr><td><code>@RequestMapping</code></td><td>定义路由映射</td></tr>
|
||||||
|
<tr><td><code>@GetMapping</code></td><td>GET 请求映射</td></tr>
|
||||||
|
<tr><td><code>@PostMapping</code></td><td>POST 请求映射</td></tr>
|
||||||
|
<tr><td><code>@PathVariable</code></td><td>获取路径变量</td></tr>
|
||||||
|
<tr><td><code>@RequestBody</code></td><td>获取请求体 JSON</td></tr>
|
||||||
|
<tr><td><code>@RequestParam</code></td><td>获取查询参数</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><a href="/">← 返回学习中心</a></p>
|
||||||
|
|
||||||
|
<!-- 添加/编辑用户模态框 -->
|
||||||
|
<div class="modal" id="userModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="modalTitle">添加用户</h3>
|
||||||
|
<button class="close-btn" onclick="closeModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<form id="userForm">
|
||||||
|
<input type="hidden" id="userId">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>姓名</label>
|
||||||
|
<input type="text" id="userName" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>邮箱</label>
|
||||||
|
<input type="email" id="userEmail" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>年龄</label>
|
||||||
|
<input type="number" id="userAge" required>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="submit" class="btn btn-primary">保存</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="closeModal()">取消</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 加载用户列表
|
||||||
|
async function loadUsers() {
|
||||||
|
const res = await fetch('/api/users');
|
||||||
|
const users = await res.json();
|
||||||
|
const tbody = document.querySelector('#userTable tbody');
|
||||||
|
tbody.innerHTML = users.map(u => `
|
||||||
|
<tr>
|
||||||
|
<td>${u.id}</td>
|
||||||
|
<td>${u.name}</td>
|
||||||
|
<td>${u.email}</td>
|
||||||
|
<td>${u.age}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-primary" onclick="editUser(${u.id}, '${u.name}', '${u.email}', ${u.age})">编辑</button>
|
||||||
|
<button class="btn btn-danger" onclick="deleteUser(${u.id})">删除</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开模态框
|
||||||
|
function openModal() {
|
||||||
|
document.getElementById('userModal').classList.add('active');
|
||||||
|
document.getElementById('modalTitle').textContent = '添加用户';
|
||||||
|
document.getElementById('userForm').reset();
|
||||||
|
document.getElementById('userId').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭模态框
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById('userModal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑用户
|
||||||
|
function editUser(id, name, email, age) {
|
||||||
|
document.getElementById('userModal').classList.add('active');
|
||||||
|
document.getElementById('modalTitle').textContent = '编辑用户';
|
||||||
|
document.getElementById('userId').value = id;
|
||||||
|
document.getElementById('userName').value = name;
|
||||||
|
document.getElementById('userEmail').value = email;
|
||||||
|
document.getElementById('userAge').value = age;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存用户
|
||||||
|
document.getElementById('userForm').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const id = document.getElementById('userId').value;
|
||||||
|
const user = {
|
||||||
|
name: document.getElementById('userName').value,
|
||||||
|
email: document.getElementById('userEmail').value,
|
||||||
|
age: parseInt(document.getElementById('userAge').value)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
await fetch(`/api/users/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ ...user, id: parseInt(id) })
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await fetch('/api/users', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(user)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal();
|
||||||
|
loadUsers();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
async function deleteUser(id) {
|
||||||
|
if (confirm('确定删除此用户?')) {
|
||||||
|
await fetch(`/api/users/${id}`, { method: 'DELETE' });
|
||||||
|
loadUsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
loadUsers();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2
target/classes/application.properties
Normal file
2
target/classes/application.properties
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
server.port=8082
|
||||||
|
spring.application.name=springboot-demo
|
||||||
BIN
target/classes/com/example/demo/DemoApplication.class
Normal file
BIN
target/classes/com/example/demo/DemoApplication.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/demo/aop/LoggingAspect.class
Normal file
BIN
target/classes/com/example/demo/aop/LoggingAspect.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/demo/aop/PerformanceAspect.class
Normal file
BIN
target/classes/com/example/demo/aop/PerformanceAspect.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/demo/aop/RateLimitAspect.class
Normal file
BIN
target/classes/com/example/demo/aop/RateLimitAspect.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/demo/aop/RateLimited.class
Normal file
BIN
target/classes/com/example/demo/aop/RateLimited.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/example/demo/controller/LearnController.class
Normal file
BIN
target/classes/com/example/demo/controller/LearnController.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/demo/controller/PageController.class
Normal file
BIN
target/classes/com/example/demo/controller/PageController.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/demo/controller/UserController.class
Normal file
BIN
target/classes/com/example/demo/controller/UserController.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/demo/event/UserEventListener.class
Normal file
BIN
target/classes/com/example/demo/event/UserEventListener.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/demo/event/UserEventPublisher.class
Normal file
BIN
target/classes/com/example/demo/event/UserEventPublisher.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/demo/model/User.class
Normal file
BIN
target/classes/com/example/demo/model/User.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/demo/model/UserEvent$Type.class
Normal file
BIN
target/classes/com/example/demo/model/UserEvent$Type.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/demo/model/UserEvent.class
Normal file
BIN
target/classes/com/example/demo/model/UserEvent.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/demo/service/UserService.class
Normal file
BIN
target/classes/com/example/demo/service/UserService.class
Normal file
Binary file not shown.
177
target/classes/static/aop.html
Normal file
177
target/classes/static/aop.html
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>AOP 切面编程 - Spring Boot</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
|
||||||
|
h1 { color: #6DB33F; margin: 20px 0; }
|
||||||
|
h2 { color: #333; border-bottom: 2px solid #6DB33F; padding-bottom: 10px; margin: 20px 0 15px; }
|
||||||
|
h3 { color: #6DB33F; margin: 15px 0 10px; }
|
||||||
|
.card { background: white; padding: 25px; margin: 15px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.btn { display: inline-block; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 500; cursor: pointer; border: none; }
|
||||||
|
.btn-primary { background: #6DB33F; color: white; }
|
||||||
|
.btn-primary:hover { background: #5da32f; }
|
||||||
|
.btn-info { background: #17a2b8; color: white; }
|
||||||
|
code { background: #f0f0f0; padding: 2px 8px; border-radius: 4px; font-family: 'Fira Code', monospace; }
|
||||||
|
pre { background: #2d2d2d; color: #f8f8f2; padding: 20px; border-radius: 8px; overflow-x: auto; margin: 15px 0; }
|
||||||
|
pre code { background: none; color: inherit; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
|
||||||
|
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
|
||||||
|
th { background: #6DB33F; color: white; }
|
||||||
|
tr:nth-child(even) { background: #f9f9f9; }
|
||||||
|
.tip { background: #e7f3ff; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #6DB33F; }
|
||||||
|
.warn { background: #fff3cd; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #ffc107; }
|
||||||
|
.result-box { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 8px; font-family: monospace; white-space: pre-wrap; margin-top: 10px; }
|
||||||
|
.nav { margin-bottom: 20px; }
|
||||||
|
.nav a { margin-right: 15px; color: #6DB33F; text-decoration: none; }
|
||||||
|
.nav a:hover { text-decoration: underline; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="nav">
|
||||||
|
<a href="/">← 返回首页</a>
|
||||||
|
<a href="/users.html">用户管理</a>
|
||||||
|
<a href="/events.html">事件机制</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>🔪 AOP 切面编程</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>📊 实时性能统计</h3>
|
||||||
|
<p>AOP 自动统计所有 Controller 和 Service 方法的执行时间</p>
|
||||||
|
<button class="btn btn-primary" onclick="loadStats()">刷新统计数据</button>
|
||||||
|
<div class="result-box" id="statsResult">点击按钮查看...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>📚 AOP 核心概念</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>1. 什么是 AOP?</h3>
|
||||||
|
<p>AOP (Aspect-Oriented Programming) 面向切面编程,是将<strong>横切关注点</strong>与<strong>业务逻辑</strong>分离的编程范式。</p>
|
||||||
|
<div class="tip">
|
||||||
|
<strong>横切关注点:</strong>日志、事务、安全、性能监控等,散布在多个模块中的公共功能。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>2. 核心术语</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>术语</th><th>说明</th></tr>
|
||||||
|
<tr><td><strong>Aspect (切面)</strong></td><td>横切关注点的模块化封装</td></tr>
|
||||||
|
<tr><td><strong>JoinPoint (连接点)</strong></td><td>程序执行的某个点(方法调用、异常抛出等)</td></tr>
|
||||||
|
<tr><td><strong>Pointcut (切入点)</strong></td><td>匹配连接点的表达式</td></tr>
|
||||||
|
<tr><td><strong>Advice (通知)</strong></td><td>在连接点执行的动作</td></tr>
|
||||||
|
<tr><td><strong>Weaving (织入)</strong></td><td>将切面应用到目标对象的过程</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>3. 五种通知类型</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>注解</th><th>执行时机</th><th>用途</th></tr>
|
||||||
|
<tr><td><code>@Before</code></td><td>方法执行前</td><td>参数校验、权限检查</td></tr>
|
||||||
|
<tr><td><code>@After</code></td><td>方法执行后(无论成功或异常)</td><td>资源清理</td></tr>
|
||||||
|
<tr><td><code>@AfterReturning</code></td><td>方法成功返回后</td><td>结果处理、日志记录</td></tr>
|
||||||
|
<tr><td><code>@AfterThrowing</code></td><td>方法抛出异常后</td><td>异常处理、错误日志</td></tr>
|
||||||
|
<tr><td><code>@Around</code></td><td>环绕方法执行(最强大)</td><td>性能统计、事务管理</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>💻 代码示例</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>日志切面示例</h3>
|
||||||
|
<pre><code>@Aspect
|
||||||
|
@Component
|
||||||
|
public class LoggingAspect {
|
||||||
|
|
||||||
|
// 切入点:匹配所有 Controller 方法
|
||||||
|
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
|
||||||
|
public void controllerMethods() {}
|
||||||
|
|
||||||
|
// 前置通知
|
||||||
|
@Before("controllerMethods()")
|
||||||
|
public void logBefore(JoinPoint jp) {
|
||||||
|
System.out.println("[AOP-Before] 方法开始: " + jp.getSignature().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回通知
|
||||||
|
@AfterReturning(pointcut = "controllerMethods()", returning = "result")
|
||||||
|
public void logAfterReturning(JoinPoint jp, Object result) {
|
||||||
|
System.out.println("[AOP-AfterReturning] 返回值: " + result);
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>性能监控切面 (@Around)</h3>
|
||||||
|
<pre><code>@Aspect
|
||||||
|
@Component
|
||||||
|
public class PerformanceAspect {
|
||||||
|
|
||||||
|
@Around("execution(* com.example.demo..*.*(..))")
|
||||||
|
public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Object result = pjp.proceed(); // 执行目标方法
|
||||||
|
long duration = System.currentTimeMillis() - start;
|
||||||
|
System.out.println("[AOP] " + pjp.getSignature() + " 耗时: " + duration + "ms");
|
||||||
|
return result;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
System.out.println("[AOP] 方法异常: " + e.getMessage());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>切入点表达式语法</h3>
|
||||||
|
<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>
|
||||||
|
async function loadStats() {
|
||||||
|
const res = await fetch('/aop/stats');
|
||||||
|
const data = await res.json();
|
||||||
|
document.getElementById('statsResult').textContent = JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
213
target/classes/static/events.html
Normal file
213
target/classes/static/events.html
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>事件机制 - Spring Boot</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
|
||||||
|
h1 { color: #6DB33F; margin: 20px 0; }
|
||||||
|
h2 { color: #333; border-bottom: 2px solid #6DB33F; padding-bottom: 10px; margin: 20px 0 15px; }
|
||||||
|
h3 { color: #6DB33F; margin: 15px 0 10px; }
|
||||||
|
.card { background: white; padding: 25px; margin: 15px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.btn { display: inline-block; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 500; cursor: pointer; border: none; margin: 5px; }
|
||||||
|
.btn-primary { background: #6DB33F; color: white; }
|
||||||
|
.btn-primary:hover { background: #5da32f; }
|
||||||
|
.btn-info { background: #17a2b8; color: white; }
|
||||||
|
.btn-warning { background: #ffc107; color: #333; }
|
||||||
|
code { background: #f0f0f0; padding: 2px 8px; border-radius: 4px; font-family: 'Fira Code', monospace; }
|
||||||
|
pre { background: #2d2d2d; color: #f8f8f2; padding: 20px; border-radius: 8px; overflow-x: auto; margin: 15px 0; }
|
||||||
|
pre code { background: none; color: inherit; }
|
||||||
|
.tip { background: #e7f3ff; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #6DB33F; }
|
||||||
|
.result-box { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 8px; font-family: monospace; white-space: pre-wrap; margin-top: 10px; max-height: 300px; overflow-y: auto; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
|
||||||
|
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
|
||||||
|
th { background: #6DB33F; color: white; }
|
||||||
|
.event-flow { display: flex; align-items: center; justify-content: center; margin: 20px 0; }
|
||||||
|
.event-flow span { padding: 15px 25px; background: #6DB33F; color: white; border-radius: 8px; margin: 0 10px; }
|
||||||
|
.event-flow .arrow { font-size: 24px; color: #6DB33F; }
|
||||||
|
.nav { margin-bottom: 20px; }
|
||||||
|
.nav a { margin-right: 15px; color: #6DB33F; text-decoration: none; }
|
||||||
|
.nav a:hover { text-decoration: underline; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="nav">
|
||||||
|
<a href="/">← 返回首页</a>
|
||||||
|
<a href="/users.html">用户管理</a>
|
||||||
|
<a href="/aop.html">AOP 切面</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>📡 Spring 事件机制</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>🎉 事件发布演示</h3>
|
||||||
|
<p>模拟用户登录事件,观察事件发布和监听过程</p>
|
||||||
|
<div style="margin: 15px 0;">
|
||||||
|
<input type="text" id="userName" placeholder="用户名" value="张三" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; width: 150px;">
|
||||||
|
<input type="number" id="userId" placeholder="用户ID" value="1" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; width: 100px;">
|
||||||
|
<button class="btn btn-primary" onclick="publishEvent()">发布登录事件</button>
|
||||||
|
</div>
|
||||||
|
<div class="result-box" id="eventResult">等待事件发布...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>🔄 事件机制流程</h2>
|
||||||
|
|
||||||
|
<div class="event-flow">
|
||||||
|
<span>发布者</span>
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<span>ApplicationEventPublisher</span>
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<span>事件</span>
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<span>监听者</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tip">
|
||||||
|
<strong>核心优势:</strong>
|
||||||
|
<ul style="margin-top: 10px; padding-left: 20px;">
|
||||||
|
<li><strong>解耦:</strong>发布者和监听者互不依赖</li>
|
||||||
|
<li><strong>扩展:</strong>新增监听器无需修改发布者</li>
|
||||||
|
<li><strong>异步:</strong>耗时操作不阻塞主流程</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>💻 代码实现</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>1. 定义事件</h3>
|
||||||
|
<pre><code>public class UserEvent {
|
||||||
|
public enum Type { CREATED, UPDATED, DELETED, LOGIN }
|
||||||
|
|
||||||
|
private Type type;
|
||||||
|
private Long userId;
|
||||||
|
private String userName;
|
||||||
|
private LocalDateTime timestamp;
|
||||||
|
|
||||||
|
// constructor, getters...
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>2. 发布事件</h3>
|
||||||
|
<pre><code>@Component
|
||||||
|
public class UserEventPublisher {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
public void publishUserLogin(Long userId, String userName) {
|
||||||
|
UserEvent event = new UserEvent(
|
||||||
|
UserEvent.Type.LOGIN,
|
||||||
|
userId,
|
||||||
|
userName,
|
||||||
|
"用户登录成功"
|
||||||
|
);
|
||||||
|
eventPublisher.publishEvent(event);
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>3. 监听事件</h3>
|
||||||
|
<pre><code>@Component
|
||||||
|
public class UserEventListener {
|
||||||
|
|
||||||
|
// 基础监听
|
||||||
|
@EventListener
|
||||||
|
public void handleUserEvent(UserEvent event) {
|
||||||
|
System.out.println("收到事件: " + event.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 条件监听 - 只处理登录事件
|
||||||
|
@EventListener(condition = "#event.type == T(com.example.demo.model.UserEvent$Type).LOGIN")
|
||||||
|
public void handleLogin(UserEvent event) {
|
||||||
|
System.out.println("用户登录: " + event.getUserName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步监听 - 不阻塞主流程
|
||||||
|
@Async
|
||||||
|
@EventListener
|
||||||
|
public void sendWelcomeEmail(UserEvent event) {
|
||||||
|
// 发送邮件...
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>4. 控制器中使用</h3>
|
||||||
|
<pre><code>@RestController
|
||||||
|
@RequestMapping("/aop")
|
||||||
|
public class AopEventController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
@PostMapping("/event/publish")
|
||||||
|
public Map<String, Object> publishEvent(
|
||||||
|
@RequestParam Long userId,
|
||||||
|
@RequestParam String userName) {
|
||||||
|
|
||||||
|
// 发布事件
|
||||||
|
eventPublisher.publishUserLogin(userId, userName);
|
||||||
|
|
||||||
|
return Map.of(
|
||||||
|
"message", "事件已发布",
|
||||||
|
"userId", userId,
|
||||||
|
"userName", userName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>🎯 应用场景</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<table>
|
||||||
|
<tr><th>场景</th><th>事件类型</th><th>处理逻辑</th></tr>
|
||||||
|
<tr><td>用户注册</td><td>UserCreatedEvent</td><td>发送欢迎邮件、初始化数据</td></tr>
|
||||||
|
<tr><td>订单创建</td><td>OrderCreatedEvent</td><td>扣库存、发送通知</td></tr>
|
||||||
|
<tr><td>支付成功</td><td>PaymentSuccessEvent</td><td>更新订单状态、发送短信</td></tr>
|
||||||
|
<tr><td>用户登录</td><td>UserLoginEvent</td><td>记录登录日志、更新在线状态</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>💡 最佳实践</h3>
|
||||||
|
<ul style="line-height: 2; padding-left: 20px;">
|
||||||
|
<li>事件类应该是<strong>不可变</strong>的(只读属性)</li>
|
||||||
|
<li>使用 <code>@Async</code> 处理耗时操作</li>
|
||||||
|
<li>避免在监听器中抛出异常</li>
|
||||||
|
<li>使用 <code>condition</code> 过滤不需要的事件</li>
|
||||||
|
<li>复杂场景考虑使用消息队列(RabbitMQ/Kafka)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top: 30px;"><a href="/">← 返回学习中心</a></p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function publishEvent() {
|
||||||
|
const userId = document.getElementById('userId').value;
|
||||||
|
const userName = document.getElementById('userName').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/aop/event/publish?userId=${userId}&userName=${encodeURIComponent(userName)}`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
const resultBox = document.getElementById('eventResult');
|
||||||
|
resultBox.textContent = `[${new Date().toLocaleTimeString()}] 事件已发布\n\n` +
|
||||||
|
JSON.stringify(data, null, 2) + '\n\n' +
|
||||||
|
'📊 查看控制台日志可以看到监听器的输出:\n' +
|
||||||
|
'[EventPublisher] 发布事件: LOGIN - ' + userName + '\n' +
|
||||||
|
'[EventListener] 收到事件: LOGIN - 用户: ' + userName + '\n' +
|
||||||
|
'[LoginTracker] 记录用户登录: ' + userName;
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('eventResult').textContent = '错误: ' + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
221
target/classes/static/index.html
Normal file
221
target/classes/static/index.html
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Spring Boot 学习中心</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
|
||||||
|
h1 { color: #6DB33F; text-align: center; margin: 30px 0; font-size: 2.5em; }
|
||||||
|
h2 { color: #333; border-bottom: 3px solid #6DB33F; padding-bottom: 10px; margin: 20px 0 15px; }
|
||||||
|
.card { background: white; padding: 25px; margin: 15px 0; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
||||||
|
.card h3 { color: #6DB33F; margin-bottom: 15px; font-size: 1.3em; }
|
||||||
|
.btn-group { display: flex; flex-wrap: wrap; gap: 10px; margin: 15px 0; }
|
||||||
|
.btn { display: inline-block; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 500; transition: all 0.3s; }
|
||||||
|
.btn-primary { background: #6DB33F; color: white; }
|
||||||
|
.btn-primary:hover { background: #5da32f; transform: translateY(-2px); }
|
||||||
|
.btn-secondary { background: #333; color: white; }
|
||||||
|
.btn-secondary:hover { background: #444; }
|
||||||
|
.btn-info { background: #17a2b8; color: white; }
|
||||||
|
.btn-info:hover { background: #138496; }
|
||||||
|
.btn-warning { background: #ffc107; color: #333; }
|
||||||
|
.btn-warning:hover { background: #e0a800; }
|
||||||
|
code { background: #f0f0f0; padding: 2px 8px; border-radius: 4px; font-family: 'Fira Code', monospace; font-size: 14px; }
|
||||||
|
pre { background: #2d2d2d; color: #f8f8f2; padding: 20px; border-radius: 8px; overflow-x: auto; margin: 15px 0; }
|
||||||
|
pre code { background: none; color: inherit; }
|
||||||
|
ul { line-height: 2; padding-left: 20px; }
|
||||||
|
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; }
|
||||||
|
.feature-item { background: #f8f9fa; padding: 20px; border-radius: 8px; border-left: 4px solid #6DB33F; transition: all 0.3s; }
|
||||||
|
.feature-item:hover { transform: translateY(-3px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
||||||
|
.feature-item h4 { color: #333; margin-bottom: 8px; font-size: 1.1em; }
|
||||||
|
.feature-item p { color: #666; font-size: 14px; margin: 0; }
|
||||||
|
.feature-item a { color: inherit; text-decoration: none; }
|
||||||
|
.api-test { background: #f8f9fa; padding: 15px; margin: 10px 0; border-radius: 8px; }
|
||||||
|
.api-test input, .api-test select { padding: 10px; border: 1px solid #ddd; border-radius: 4px; margin: 5px; }
|
||||||
|
.api-test button { padding: 10px 20px; background: #6DB33F; color: white; border: none; border-radius: 4px; cursor: pointer; }
|
||||||
|
.api-test button:hover { background: #5da32f; }
|
||||||
|
#result { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 8px; margin-top: 10px; white-space: pre-wrap; font-family: monospace; font-size: 14px; }
|
||||||
|
.footer { text-align: center; margin-top: 40px; padding: 20px; color: #666; border-top: 1px solid #ddd; }
|
||||||
|
.nav-links { display: flex; justify-content: center; gap: 15px; margin-bottom: 30px; }
|
||||||
|
.nav-links a { padding: 10px 20px; background: white; border-radius: 8px; text-decoration: none; color: #6DB33F; font-weight: 500; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.nav-links a:hover { background: #6DB33F; color: white; }
|
||||||
|
.nav-links a.active { background: #6DB33F; color: white; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🍃 Spring Boot 学习中心</h1>
|
||||||
|
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="/" class="active">首页</a>
|
||||||
|
<a href="/users.html">用户管理</a>
|
||||||
|
<a href="/aop.html">AOP 切面</a>
|
||||||
|
<a href="/events.html">事件机制</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>📚 学习模块</h3>
|
||||||
|
<div class="feature-grid">
|
||||||
|
<a href="/users.html" class="feature-item">
|
||||||
|
<h4>👥 用户管理</h4>
|
||||||
|
<p>RESTful API 设计、CRUD 操作、参数绑定</p>
|
||||||
|
</a>
|
||||||
|
<a href="/aop.html" class="feature-item">
|
||||||
|
<h4>🔪 AOP 切面编程</h4>
|
||||||
|
<p>日志记录、性能监控、限流控制</p>
|
||||||
|
</a>
|
||||||
|
<a href="/events.html" class="feature-item">
|
||||||
|
<h4>📡 事件机制</h4>
|
||||||
|
<p>发布/订阅模式、解耦业务逻辑</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>🔗 快速链接</h2>
|
||||||
|
<div class="card">
|
||||||
|
<div class="btn-group">
|
||||||
|
<a class="btn btn-primary" href="/learn">API 接口列表</a>
|
||||||
|
<a class="btn btn-info" href="/api/users">用户 JSON</a>
|
||||||
|
<a class="btn btn-secondary" href="/actuator/health">健康检查</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>🧪 接口测试</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>GET 参数示例</h3>
|
||||||
|
<div class="api-test">
|
||||||
|
<input type="text" id="param-name" placeholder="姓名" value="张三">
|
||||||
|
<input type="number" id="param-age" placeholder="年龄" value="25">
|
||||||
|
<button onclick="testParams()">测试</button>
|
||||||
|
<div id="result-params"></div>
|
||||||
|
</div>
|
||||||
|
<p><code>GET /learn/params?name=xxx&age=18</code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>路径变量示例</h3>
|
||||||
|
<div class="api-test">
|
||||||
|
<input type="text" id="path-id" placeholder="ID" value="123">
|
||||||
|
<button onclick="testPath()">测试</button>
|
||||||
|
<div id="result-path"></div>
|
||||||
|
</div>
|
||||||
|
<p><code>GET /learn/path/{id}</code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>POST JSON 示例</h3>
|
||||||
|
<div class="api-test">
|
||||||
|
<input type="text" id="post-data" placeholder='JSON 数据' value='{"name":"test","value":123}' style="width: 300px;">
|
||||||
|
<button onclick="testPost()">测试</button>
|
||||||
|
<div id="result-post"></div>
|
||||||
|
</div>
|
||||||
|
<p><code>POST /learn/body</code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>📖 学习路径</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>1. IOC 容器</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code>@Component</code>, <code>@Service</code>, <code>@Repository</code>, <code>@Controller</code></li>
|
||||||
|
<li><code>@Autowired</code> 依赖注入</li>
|
||||||
|
<li><code>@Configuration</code> + <code>@Bean</code> 配置类</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>2. Web 开发</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code>@RestController</code> = <code>@Controller</code> + <code>@ResponseBody</code></li>
|
||||||
|
<li><code>@RequestMapping</code>, <code>@GetMapping</code>, <code>@PostMapping</code></li>
|
||||||
|
<li><code>@PathVariable</code>, <code>@RequestParam</code>, <code>@RequestBody</code></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>3. AOP 切面编程</h3>
|
||||||
|
<pre><code>@Aspect
|
||||||
|
@Component
|
||||||
|
public class LoggingAspect {
|
||||||
|
@Before("execution(* com.example.*.*(..))")
|
||||||
|
public void logBefore(JoinPoint jp) {
|
||||||
|
System.out.println("方法调用: " + jp.getSignature());
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>4. 事件机制</h3>
|
||||||
|
<pre><code>// 发布事件
|
||||||
|
@Autowired
|
||||||
|
ApplicationEventPublisher publisher;
|
||||||
|
publisher.publishEvent(new UserEvent(...));
|
||||||
|
|
||||||
|
// 监听事件
|
||||||
|
@EventListener
|
||||||
|
public void onEvent(UserEvent event) {
|
||||||
|
// 处理事件
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>📁 项目结构</h2>
|
||||||
|
<div class="card">
|
||||||
|
<pre><code>├── src/main/java/com/example/demo/
|
||||||
|
│ ├── DemoApplication.java # 启动类
|
||||||
|
│ ├── controller/ # 控制器层
|
||||||
|
│ │ ├── LearnController.java # 学习示例
|
||||||
|
│ │ ├── UserController.java # 用户 API
|
||||||
|
│ │ └── AopEventController.java
|
||||||
|
│ ├── service/ # 业务逻辑层
|
||||||
|
│ ├── model/ # 实体类
|
||||||
|
│ ├── aop/ # AOP 切面
|
||||||
|
│ │ ├── LoggingAspect.java
|
||||||
|
│ │ ├── PerformanceAspect.java
|
||||||
|
│ │ └── RateLimitAspect.java
|
||||||
|
│ └── event/ # 事件机制
|
||||||
|
│ ├── UserEventPublisher.java
|
||||||
|
│ └── UserEventListener.java
|
||||||
|
├── src/main/resources/
|
||||||
|
│ ├── static/ # 静态资源
|
||||||
|
│ │ ├── index.html
|
||||||
|
│ │ ├── users.html
|
||||||
|
│ │ ├── aop.html
|
||||||
|
│ │ └── events.html
|
||||||
|
│ └── application.properties # 配置文件
|
||||||
|
└── pom.xml # Maven 配置</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>🍃 Spring Boot 学习脚手架 | <a href="https://spring.io" style="color: #6DB33F;">Spring 官网</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function testParams() {
|
||||||
|
const name = document.getElementById('param-name').value;
|
||||||
|
const age = document.getElementById('param-age').value;
|
||||||
|
const res = await fetch(`/learn/params?name=${encodeURIComponent(name)}&age=${age}`);
|
||||||
|
const data = await res.json();
|
||||||
|
document.getElementById('result-params').innerHTML = '<div id="result">' + JSON.stringify(data, null, 2) + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testPath() {
|
||||||
|
const id = document.getElementById('path-id').value;
|
||||||
|
const res = await fetch(`/learn/path/${id}`);
|
||||||
|
const data = await res.json();
|
||||||
|
document.getElementById('result-path').innerHTML = '<div id="result">' + JSON.stringify(data, null, 2) + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testPost() {
|
||||||
|
const data = document.getElementById('post-data').value;
|
||||||
|
const res = await fetch('/learn/body', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: data
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
document.getElementById('result-post').innerHTML = '<div id="result">' + JSON.stringify(result, null, 2) + '</div>';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
225
target/classes/static/users.html
Normal file
225
target/classes/static/users.html
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>用户管理 - Spring Boot</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
|
||||||
|
h1 { color: #6DB33F; margin: 20px 0; }
|
||||||
|
h2 { color: #333; margin: 20px 0 10px; }
|
||||||
|
.card { background: white; padding: 20px; margin: 15px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
|
||||||
|
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
|
||||||
|
th { background: #6DB33F; color: white; }
|
||||||
|
tr:nth-child(even) { background: #f9f9f9; }
|
||||||
|
tr:hover { background: #f0f0f0; }
|
||||||
|
.btn { display: inline-block; padding: 8px 16px; border-radius: 4px; text-decoration: none; font-size: 14px; cursor: pointer; border: none; }
|
||||||
|
.btn-primary { background: #6DB33F; color: white; }
|
||||||
|
.btn-primary:hover { background: #5da32f; }
|
||||||
|
.btn-danger { background: #dc3545; color: white; }
|
||||||
|
.btn-danger:hover { background: #c82333; }
|
||||||
|
.btn-secondary { background: #6c757d; color: white; }
|
||||||
|
.btn-success { background: #28a745; color: white; }
|
||||||
|
.form-group { margin: 15px 0; }
|
||||||
|
.form-group label { display: block; margin-bottom: 5px; font-weight: bold; }
|
||||||
|
.form-group input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
|
||||||
|
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); }
|
||||||
|
.modal.active { display: flex; justify-content: center; align-items: center; }
|
||||||
|
.modal-content { background: white; padding: 30px; border-radius: 8px; width: 400px; }
|
||||||
|
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||||
|
.modal-header h3 { margin: 0; }
|
||||||
|
.close-btn { background: none; border: none; font-size: 24px; cursor: pointer; }
|
||||||
|
.btn-group { display: flex; gap: 10px; margin-top: 20px; }
|
||||||
|
.tip { background: #e7f3ff; padding: 15px; border-radius: 4px; margin: 15px 0; border-left: 4px solid #6DB33F; }
|
||||||
|
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; }
|
||||||
|
pre { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 4px; overflow-x: auto; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>👥 用户管理 - RESTful API 示例</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<h2>用户列表</h2>
|
||||||
|
<button class="btn btn-primary" onclick="openModal()">+ 添加用户</button>
|
||||||
|
</div>
|
||||||
|
<table id="userTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>姓名</th>
|
||||||
|
<th>邮箱</th>
|
||||||
|
<th>年龄</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>📖 学习要点</h2>
|
||||||
|
<div class="tip">
|
||||||
|
<strong>RESTful API 设计:</strong>
|
||||||
|
<ul style="margin-top: 10px; padding-left: 20px;">
|
||||||
|
<li><code>GET /api/users</code> - 获取所有用户</li>
|
||||||
|
<li><code>GET /api/users/{id}</code> - 获取单个用户</li>
|
||||||
|
<li><code>POST /api/users</code> - 创建用户</li>
|
||||||
|
<li><code>PUT /api/users/{id}</code> - 更新用户</li>
|
||||||
|
<li><code>DELETE /api/users/{id}</code> - 删除用户</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Controller 代码示例</h3>
|
||||||
|
<pre><code>@RestController
|
||||||
|
@RequestMapping("/api/users")
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<User> getAllUsers() { ... }
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public User getUserById(@PathVariable Long id) { ... }
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public User createUser(@RequestBody User user) { ... }
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public User updateUser(@PathVariable Long id, @RequestBody User user) { ... }
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public String deleteUser(@PathVariable Long id) { ... }
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>🔧 Spring 注解说明</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th>注解</th><th>说明</th></tr>
|
||||||
|
<tr><td><code>@RestController</code></td><td>= @Controller + @ResponseBody</td></tr>
|
||||||
|
<tr><td><code>@RequestMapping</code></td><td>定义路由映射</td></tr>
|
||||||
|
<tr><td><code>@GetMapping</code></td><td>GET 请求映射</td></tr>
|
||||||
|
<tr><td><code>@PostMapping</code></td><td>POST 请求映射</td></tr>
|
||||||
|
<tr><td><code>@PathVariable</code></td><td>获取路径变量</td></tr>
|
||||||
|
<tr><td><code>@RequestBody</code></td><td>获取请求体 JSON</td></tr>
|
||||||
|
<tr><td><code>@RequestParam</code></td><td>获取查询参数</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><a href="/">← 返回学习中心</a></p>
|
||||||
|
|
||||||
|
<!-- 添加/编辑用户模态框 -->
|
||||||
|
<div class="modal" id="userModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="modalTitle">添加用户</h3>
|
||||||
|
<button class="close-btn" onclick="closeModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<form id="userForm">
|
||||||
|
<input type="hidden" id="userId">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>姓名</label>
|
||||||
|
<input type="text" id="userName" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>邮箱</label>
|
||||||
|
<input type="email" id="userEmail" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>年龄</label>
|
||||||
|
<input type="number" id="userAge" required>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="submit" class="btn btn-primary">保存</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="closeModal()">取消</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 加载用户列表
|
||||||
|
async function loadUsers() {
|
||||||
|
const res = await fetch('/api/users');
|
||||||
|
const users = await res.json();
|
||||||
|
const tbody = document.querySelector('#userTable tbody');
|
||||||
|
tbody.innerHTML = users.map(u => `
|
||||||
|
<tr>
|
||||||
|
<td>${u.id}</td>
|
||||||
|
<td>${u.name}</td>
|
||||||
|
<td>${u.email}</td>
|
||||||
|
<td>${u.age}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-primary" onclick="editUser(${u.id}, '${u.name}', '${u.email}', ${u.age})">编辑</button>
|
||||||
|
<button class="btn btn-danger" onclick="deleteUser(${u.id})">删除</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开模态框
|
||||||
|
function openModal() {
|
||||||
|
document.getElementById('userModal').classList.add('active');
|
||||||
|
document.getElementById('modalTitle').textContent = '添加用户';
|
||||||
|
document.getElementById('userForm').reset();
|
||||||
|
document.getElementById('userId').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭模态框
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById('userModal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑用户
|
||||||
|
function editUser(id, name, email, age) {
|
||||||
|
document.getElementById('userModal').classList.add('active');
|
||||||
|
document.getElementById('modalTitle').textContent = '编辑用户';
|
||||||
|
document.getElementById('userId').value = id;
|
||||||
|
document.getElementById('userName').value = name;
|
||||||
|
document.getElementById('userEmail').value = email;
|
||||||
|
document.getElementById('userAge').value = age;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存用户
|
||||||
|
document.getElementById('userForm').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const id = document.getElementById('userId').value;
|
||||||
|
const user = {
|
||||||
|
name: document.getElementById('userName').value,
|
||||||
|
email: document.getElementById('userEmail').value,
|
||||||
|
age: parseInt(document.getElementById('userAge').value)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
await fetch(`/api/users/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ ...user, id: parseInt(id) })
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await fetch('/api/users', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(user)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal();
|
||||||
|
loadUsers();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
async function deleteUser(id) {
|
||||||
|
if (confirm('确定删除此用户?')) {
|
||||||
|
await fetch(`/api/users/${id}`, { method: 'DELETE' });
|
||||||
|
loadUsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
loadUsers();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
target/demo-0.0.1-SNAPSHOT.jar
Normal file
BIN
target/demo-0.0.1-SNAPSHOT.jar
Normal file
Binary file not shown.
BIN
target/demo-0.0.1-SNAPSHOT.jar.original
Normal file
BIN
target/demo-0.0.1-SNAPSHOT.jar.original
Normal file
Binary file not shown.
3
target/maven-archiver/pom.properties
Normal file
3
target/maven-archiver/pom.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
artifactId=demo
|
||||||
|
groupId=com.example
|
||||||
|
version=0.0.1-SNAPSHOT
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
com/example/demo/aop/RateLimited.class
|
||||||
|
com/example/demo/model/UserEvent.class
|
||||||
|
com/example/demo/aop/LoggingAspect.class
|
||||||
|
com/example/demo/event/UserEventPublisher.class
|
||||||
|
com/example/demo/controller/AopEventController.class
|
||||||
|
com/example/demo/controller/LearnController.class
|
||||||
|
com/example/demo/DemoApplication.class
|
||||||
|
com/example/demo/event/UserEventListener.class
|
||||||
|
com/example/demo/controller/PageController.class
|
||||||
|
com/example/demo/controller/UserController.class
|
||||||
|
com/example/demo/model/UserEvent$Type.class
|
||||||
|
com/example/demo/model/User.class
|
||||||
|
com/example/demo/aop/RateLimitAspect.class
|
||||||
|
com/example/demo/service/UserService.class
|
||||||
|
com/example/demo/aop/PerformanceAspect.class
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/home/llm/projects/springboot-demo/src/main/java/com/example/demo/controller/UserController.java
|
||||||
|
/home/llm/projects/springboot-demo/src/main/java/com/example/demo/model/User.java
|
||||||
|
/home/llm/projects/springboot-demo/src/main/java/com/example/demo/aop/LoggingAspect.java
|
||||||
|
/home/llm/projects/springboot-demo/src/main/java/com/example/demo/controller/PageController.java
|
||||||
|
/home/llm/projects/springboot-demo/src/main/java/com/example/demo/aop/RateLimited.java
|
||||||
|
/home/llm/projects/springboot-demo/src/main/java/com/example/demo/aop/PerformanceAspect.java
|
||||||
|
/home/llm/projects/springboot-demo/src/main/java/com/example/demo/service/UserService.java
|
||||||
|
/home/llm/projects/springboot-demo/src/main/java/com/example/demo/controller/AopEventController.java
|
||||||
|
/home/llm/projects/springboot-demo/src/main/java/com/example/demo/event/UserEventPublisher.java
|
||||||
|
/home/llm/projects/springboot-demo/src/main/java/com/example/demo/DemoApplication.java
|
||||||
|
/home/llm/projects/springboot-demo/src/main/java/com/example/demo/event/UserEventListener.java
|
||||||
|
/home/llm/projects/springboot-demo/src/main/java/com/example/demo/controller/LearnController.java
|
||||||
|
/home/llm/projects/springboot-demo/src/main/java/com/example/demo/model/UserEvent.java
|
||||||
|
/home/llm/projects/springboot-demo/src/main/java/com/example/demo/aop/RateLimitAspect.java
|
||||||
Reference in New Issue
Block a user