From bde4f6b9cfea21d91c797b377bf59128a2f9f436 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 23 Mar 2026 13:05:44 +0800 Subject: [PATCH] feat: finish bilingual learning auth cockpit --- .gitignore | 1 + .../com/example/demo/common/ApiResponse.java | 2 +- .../controller/AdvancedLabController.java | 180 ++++ .../auth/LearningAuthController.java | 56 +- .../demo/security/LearningJwtFilter.java | 31 +- .../demo/security/LearningJwtUtil.java | 4 + .../demo/security/LearningSecurityConfig.java | 6 +- src/main/resources/static/access.html | 389 ++++++++ src/main/resources/static/aop.html | 218 ++++- src/main/resources/static/events.html | 208 ++++- src/main/resources/static/index.html | 409 +++++++-- src/main/resources/static/learning-shell.js | 329 +++++++ src/main/resources/static/users.html | 449 +++++++--- .../controller/AdvancedLabControllerTest.java | 66 ++ .../example/demo/controller/AuthFlowTest.java | 47 +- .../demo/controller/UserControllerTest.java | 39 +- .../com/example/demo/aop/LoggingAspect.class | Bin 3299 -> 3299 bytes .../example/demo/aop/PerformanceAspect.class | Bin 4116 -> 4090 bytes .../example/demo/aop/RateLimitAspect.class | Bin 4052 -> 4052 bytes .../com/example/demo/common/ApiResponse.class | Bin 3152 -> 3161 bytes .../controller/AdvancedLabController.class | Bin 0 -> 14832 bytes .../controller/AopEventController$1.class | Bin 0 -> 944 bytes .../demo/controller/AopEventController.class | Bin 5818 -> 9266 bytes .../demo/controller/LearnController.class | Bin 5154 -> 5852 bytes .../demo/controller/UserController.class | Bin 4157 -> 4672 bytes .../auth/LearningAuthController.class | Bin 3054 -> 4340 bytes .../com/example/demo/dto/UserRequest.class | Bin 2719 -> 3055 bytes .../example/demo/dto/UserStatsResponse.class | Bin 0 -> 1848 bytes .../example/demo/dto/auth/LoginRequest.class | Bin 2043 -> 2043 bytes .../demo/event/UserEventListener.class | Bin 3501 -> 3511 bytes .../demo/event/UserEventPublisher.class | Bin 2473 -> 2717 bytes .../exception/DuplicateEmailException.class | Bin 0 -> 452 bytes .../exception/GlobalExceptionHandler.class | Bin 4277 -> 4377 bytes .../demo/security/LearningJwtFilter.class | Bin 3620 -> 4487 bytes .../demo/security/LearningJwtUtil.class | Bin 3083 -> 3357 bytes .../security/LearningSecurityConfig.class | Bin 5006 -> 5056 bytes .../example/demo/service/UserService.class | Bin 5385 -> 8542 bytes target/classes/static/access.html | 389 ++++++++ target/classes/static/aop.html | 535 ++++++----- target/classes/static/events.html | 576 +++++++----- target/classes/static/index.html | 838 ++++++++++++----- target/classes/static/learning-shell.js | 329 +++++++ target/classes/static/users.html | 845 +++++++++++++----- .../compile/default-compile/createdFiles.lst | 54 +- .../compile/default-compile/inputFiles.lst | 51 +- .../default-testCompile/createdFiles.lst | 5 +- .../default-testCompile/inputFiles.lst | 5 +- ...o.controller.AdvancedLabControllerTest.xml | 100 +++ ...m.example.demo.controller.AuthFlowTest.xml | 95 +- ...ple.demo.controller.UserControllerTest.xml | 156 ++-- ...o.controller.AdvancedLabControllerTest.txt | 4 + ...m.example.demo.controller.AuthFlowTest.txt | 2 +- ...ple.demo.controller.UserControllerTest.txt | 2 +- .../AdvancedLabControllerTest.class | Bin 0 -> 5078 bytes .../demo/controller/AuthFlowTest.class | Bin 4558 -> 5893 bytes .../demo/controller/UserControllerTest.class | Bin 3358 -> 6044 bytes 56 files changed, 5043 insertions(+), 1377 deletions(-) create mode 100644 .gitignore create mode 100644 src/main/java/com/example/demo/controller/AdvancedLabController.java create mode 100644 src/main/resources/static/access.html create mode 100644 src/main/resources/static/learning-shell.js create mode 100644 src/test/java/com/example/demo/controller/AdvancedLabControllerTest.java create mode 100644 target/classes/com/example/demo/controller/AdvancedLabController.class create mode 100644 target/classes/com/example/demo/controller/AopEventController$1.class create mode 100644 target/classes/com/example/demo/dto/UserStatsResponse.class create mode 100644 target/classes/com/example/demo/exception/DuplicateEmailException.class create mode 100644 target/classes/static/access.html create mode 100644 target/classes/static/learning-shell.js create mode 100644 target/surefire-reports/TEST-com.example.demo.controller.AdvancedLabControllerTest.xml create mode 100644 target/surefire-reports/com.example.demo.controller.AdvancedLabControllerTest.txt create mode 100644 target/test-classes/com/example/demo/controller/AdvancedLabControllerTest.class diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..498c250 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/surefire-reports/*.dumpstream diff --git a/src/main/java/com/example/demo/common/ApiResponse.java b/src/main/java/com/example/demo/common/ApiResponse.java index a38e218..56315f2 100644 --- a/src/main/java/com/example/demo/common/ApiResponse.java +++ b/src/main/java/com/example/demo/common/ApiResponse.java @@ -16,7 +16,7 @@ public record ApiResponse( return new ApiResponse<>(0, message, data, Instant.now()); } - public static ApiResponse fail(int code, String message) { + public static ApiResponse fail(int code, String message) { return new ApiResponse<>(code, message, null, Instant.now()); } } diff --git a/src/main/java/com/example/demo/controller/AdvancedLabController.java b/src/main/java/com/example/demo/controller/AdvancedLabController.java new file mode 100644 index 0000000..8093f49 --- /dev/null +++ b/src/main/java/com/example/demo/controller/AdvancedLabController.java @@ -0,0 +1,180 @@ +package com.example.demo.controller; + +import com.example.demo.common.ApiResponse; +import com.example.demo.security.LearningJwtUtil; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +@RestController +@RequestMapping("/api/lab") +public class AdvancedLabController { + + private final RequestMappingHandlerMapping handlerMapping; + private final LearningJwtUtil jwtUtil; + + public AdvancedLabController( + @Qualifier("requestMappingHandlerMapping") RequestMappingHandlerMapping handlerMapping, + LearningJwtUtil jwtUtil + ) { + this.handlerMapping = handlerMapping; + this.jwtUtil = jwtUtil; + } + + @GetMapping("/reflection/routes") + public ApiResponse> reflectionRoutes() { + List> routes = handlerMapping.getHandlerMethods().entrySet().stream() + .map(this::toRouteView) + .sorted((left, right) -> left.get("path").toString().compareTo(right.get("path").toString())) + .toList(); + + List> classSummaries = List.of( + summarizeClass(UserController.class), + summarizeClass(LearnController.class), + summarizeClass(AopEventController.class), + summarizeClass(com.example.demo.controller.auth.LearningAuthController.class) + ); + + return ApiResponse.ok(Map.of( + "routeCount", routes.size(), + "routes", routes, + "classSummaries", classSummaries, + "tip", "Use reflection to connect annotations, method signatures, and runtime route registration." + )); + } + + @GetMapping("/reflection/user-model") + public ApiResponse> reflectUserModel() { + List> fields = Arrays.stream(com.example.demo.model.User.class.getDeclaredFields()) + .map(this::toFieldView) + .toList(); + + return ApiResponse.ok(Map.of( + "className", com.example.demo.model.User.class.getName(), + "fieldCount", fields.size(), + "fields", fields, + "tip", "Reflection makes frameworks possible because metadata can be discovered at runtime." + )); + } + + @GetMapping("/concurrency/simulate") + public ApiResponse> simulateConcurrency( + @RequestParam(defaultValue = "12") int tasks, + @RequestParam(defaultValue = "4") int poolSize + ) { + int safeTasks = Math.max(1, Math.min(tasks, 32)); + int safePoolSize = Math.max(1, Math.min(poolSize, 8)); + ExecutorService executor = Executors.newFixedThreadPool(safePoolSize); + AtomicInteger counter = new AtomicInteger(); + Instant start = Instant.now(); + + try { + List>> jobs = createJobs(safeTasks, counter, executor); + CompletableFuture.allOf(jobs.toArray(CompletableFuture[]::new)).join(); + List> results = jobs.stream() + .map(CompletableFuture::join) + .toList(); + + return ApiResponse.ok(Map.of( + "tasks", safeTasks, + "poolSize", safePoolSize, + "finalCounter", counter.get(), + "durationMs", Duration.between(start, Instant.now()).toMillis(), + "sample", results.stream().limit(5).toList(), + "tip", "AtomicInteger keeps the shared counter safe when many tasks run in parallel." + )); + } finally { + executor.shutdownNow(); + } + } + + @GetMapping("/jwt/claims") + public ApiResponse> jwtClaims(@RequestHeader("Authorization") String authorization) { + String token = authorization.substring(7); + return ApiResponse.ok(Map.of( + "subject", jwtUtil.username(token), + "claims", jwtUtil.claims(token), + "tip", "JWT claims can be parsed without a database lookup, which is one reason token auth scales well." + )); + } + + private List>> createJobs(int tasks, AtomicInteger counter, ExecutorService executor) { + return java.util.stream.IntStream.range(0, tasks) + .mapToObj(index -> CompletableFuture.supplyAsync(() -> { + int current = counter.incrementAndGet(); + try { + Thread.sleep(20L + (index % 3) * 10L); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + } + return Map.of( + "task", index, + "thread", Thread.currentThread().getName(), + "counterAfterIncrement", current + ); + }, executor)) + .toList(); + } + + private Map toRouteView(Map.Entry entry) { + HandlerMethod handlerMethod = entry.getValue(); + return Map.of( + "path", entry.getKey().getPatternValues().toString(), + "methods", entry.getKey().getMethodsCondition().getMethods().toString(), + "handler", handlerMethod.getBeanType().getSimpleName() + "#" + handlerMethod.getMethod().getName(), + "parameters", Arrays.stream(handlerMethod.getMethod().getParameters()) + .map(parameter -> parameter.getType().getSimpleName()) + .toList() + ); + } + + private Map summarizeClass(Class type) { + List> methods = Arrays.stream(type.getDeclaredMethods()) + .filter(method -> !method.isSynthetic()) + .map(this::toMethodView) + .toList(); + + return Map.of( + "className", type.getSimpleName(), + "annotations", Arrays.stream(type.getAnnotations()).map(annotation -> annotation.annotationType().getSimpleName()).toList(), + "methodCount", methods.size(), + "methods", methods + ); + } + + private Map toMethodView(Method method) { + return Map.of( + "name", method.getName(), + "returnType", method.getReturnType().getSimpleName(), + "parameterCount", method.getParameterCount(), + "annotations", Arrays.stream(method.getAnnotations()).map(Annotation::annotationType).map(Class::getSimpleName).toList() + ); + } + + private Map toFieldView(Field field) { + return Map.of( + "name", field.getName(), + "type", field.getType().getSimpleName(), + "annotations", Arrays.stream(field.getAnnotations()).map(Annotation::annotationType).map(Class::getSimpleName).toList() + ); + } +} diff --git a/src/main/java/com/example/demo/controller/auth/LearningAuthController.java b/src/main/java/com/example/demo/controller/auth/LearningAuthController.java index a276005..8e07158 100644 --- a/src/main/java/com/example/demo/controller/auth/LearningAuthController.java +++ b/src/main/java/com/example/demo/controller/auth/LearningAuthController.java @@ -4,7 +4,14 @@ import com.example.demo.common.ApiResponse; import com.example.demo.dto.auth.LoginRequest; import com.example.demo.security.LearningJwtUtil; import jakarta.validation.Valid; -import org.springframework.web.bind.annotation.*; +import org.springframework.http.HttpHeaders; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import java.util.Map; @@ -19,27 +26,48 @@ public class LearningAuthController { } @PostMapping("/login") - public ApiResponse> login(@Valid @RequestBody LoginRequest req) { - // 学习演示:仅做最小账号检查 - if (!(("admin".equals(req.username()) && "admin123".equals(req.password())) - || ("user".equals(req.username()) && "user123".equals(req.password())))) { - return new ApiResponse<>(401, "用户名或密码错误", null, java.time.Instant.now()); + public ApiResponse> login(@Valid @RequestBody LoginRequest request) { + if (!(("admin".equals(request.username()) && "admin123".equals(request.password())) + || ("user".equals(request.username()) && "user123".equals(request.password())))) { + return new ApiResponse<>(401, "Invalid demo credentials", null, java.time.Instant.now()); } - String token = jwtUtil.generateToken(req.username()); + + String token = jwtUtil.generateToken(request.username()); return ApiResponse.ok(Map.of( - "token", token, - "type", "Bearer", - "username", req.username(), - "tip", "在请求头中加入 Authorization: Bearer 访问 /api/secure/**" + "token", token, + "type", "Bearer", + "username", request.username(), + "tip", "Attach Authorization: Bearer when calling protected lab APIs." )); } @GetMapping("/mode") public ApiResponse> mode() { return ApiResponse.ok(Map.of( - "mode", "learning-jwt", - "protectedPath", "/api/secure/**", - "defaultAccounts", "admin/admin123, user/user123" + "mode", "learning-jwt", + "protectedPaths", new String[]{"/api/users/**", "/aop/**", "/api/lab/**", "/learn/**", "/api/secure/**"}, + "defaultAccounts", new String[]{"admin/admin123", "user/user123"}, + "note", "Use this demo login before opening the advanced labs on a public VPS." + )); + } + + @GetMapping("/introspect") + public ApiResponse> introspect( + @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization + ) { + if (!StringUtils.hasText(authorization) || !authorization.startsWith("Bearer ")) { + return ApiResponse.fail(401, "Missing bearer token"); + } + + String token = authorization.substring(7); + if (!jwtUtil.validate(token)) { + return ApiResponse.fail(401, "Token is invalid or expired"); + } + + return ApiResponse.ok(Map.of( + "subject", jwtUtil.username(token), + "claims", jwtUtil.claims(token), + "tip", "JWT is self-contained: the server can read claims without storing a session row for each request." )); } } diff --git a/src/main/java/com/example/demo/security/LearningJwtFilter.java b/src/main/java/com/example/demo/security/LearningJwtFilter.java index 2644f39..6c50136 100644 --- a/src/main/java/com/example/demo/security/LearningJwtFilter.java +++ b/src/main/java/com/example/demo/security/LearningJwtFilter.java @@ -26,37 +26,46 @@ public class LearningJwtFilter extends OncePerRequestFilter { @Override protected boolean shouldNotFilter(HttpServletRequest request) { - return !request.getRequestURI().startsWith("/api/secure/"); + String uri = request.getRequestURI(); + boolean learnRoute = "/learn".equals(uri) || uri.startsWith("/learn/"); + return !(uri.startsWith("/api/secure/") + || uri.equals("/api/users") + || uri.startsWith("/api/users/") + || uri.startsWith("/aop/") + || uri.startsWith("/api/lab/") + || learnRoute); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { + throws ServletException, IOException { String auth = request.getHeader("Authorization"); if (!StringUtils.hasText(auth) || !auth.startsWith("Bearer ")) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write("{\"code\":401,\"message\":\"缺少或非法 Authorization\"}"); + writeUnauthorized(response, "Missing or invalid Authorization header"); return; } String token = auth.substring(7); if (!jwtUtil.validate(token)) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write("{\"code\":401,\"message\":\"Token 无效或过期\"}"); + writeUnauthorized(response, "Token is invalid or expired"); return; } String username = jwtUtil.username(token); var authToken = new UsernamePasswordAuthenticationToken( - username, - null, - List.of(new SimpleGrantedAuthority("ROLE_USER")) + username, + null, + List.of(new SimpleGrantedAuthority("ROLE_USER")) ); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); filterChain.doFilter(request, response); } + + private void writeUnauthorized(HttpServletResponse response, String message) throws IOException { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":401,\"message\":\"" + message + "\"}"); + } } diff --git a/src/main/java/com/example/demo/security/LearningJwtUtil.java b/src/main/java/com/example/demo/security/LearningJwtUtil.java index 647d811..f1c6960 100644 --- a/src/main/java/com/example/demo/security/LearningJwtUtil.java +++ b/src/main/java/com/example/demo/security/LearningJwtUtil.java @@ -48,6 +48,10 @@ public class LearningJwtUtil { return parse(token).getSubject(); } + public Map claims(String token) { + return Map.copyOf(parse(token)); + } + private Claims parse(String token) { return Jwts.parser().verifyWith(key()).build().parseSignedClaims(token).getPayload(); } diff --git a/src/main/java/com/example/demo/security/LearningSecurityConfig.java b/src/main/java/com/example/demo/security/LearningSecurityConfig.java index 9aef34b..9a525c9 100644 --- a/src/main/java/com/example/demo/security/LearningSecurityConfig.java +++ b/src/main/java/com/example/demo/security/LearningSecurityConfig.java @@ -26,10 +26,10 @@ public class LearningSecurityConfig { .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers( - "/", "/home", "/learn/**", "/aop/**", "/api/users/**", "/api/health", - "/api/auth/**", "/actuator/**", "/index.html", "/users.html", "/aop.html", "/events.html" + "/", "/home", "/api/auth/**", "/actuator/health", "/index.html", "/access.html", + "/users.html", "/aop.html", "/events.html", "/advanced.html" ).permitAll() - .requestMatchers("/api/secure/**").authenticated() + .requestMatchers("/api/secure/**", "/api/users/**", "/aop/**", "/api/lab/**", "/learn/**").authenticated() .anyRequest().permitAll() ) .addFilterBefore(learningJwtFilter, UsernamePasswordAuthenticationFilter.class); diff --git a/src/main/resources/static/access.html b/src/main/resources/static/access.html new file mode 100644 index 0000000..2101683 --- /dev/null +++ b/src/main/resources/static/access.html @@ -0,0 +1,389 @@ + + + + + + Spring Learning Login + + + +
+
+
+

+

+
+ + + + +
+
+ +
+
+
+

+

+ +
+ + +
+
+ + +
+ +
+ + + +
+ +
+

+        
+ + +
+
+ + + + + diff --git a/src/main/resources/static/aop.html b/src/main/resources/static/aop.html index beefbe3..6e34579 100644 --- a/src/main/resources/static/aop.html +++ b/src/main/resources/static/aop.html @@ -1,5 +1,5 @@ - + @@ -114,39 +114,37 @@
-
AOP Lab
-

Make cross-cutting behavior visible instead of abstract.

-

- This page focuses on the parts students usually miss: where advice wraps controller and service methods, - how timing is collected, and why a validation failure still counts as observed runtime behavior. -

+
+

+

-
Live Runs
-

Run requests and inspect collected metrics

-

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

+
+

+

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

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

+ +

- What to inspect in code -

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

+ +

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

         
+ diff --git a/src/main/resources/static/events.html b/src/main/resources/static/events.html index 16bdcad..d853654 100644 --- a/src/main/resources/static/events.html +++ b/src/main/resources/static/events.html @@ -1,5 +1,5 @@ - + @@ -129,54 +129,52 @@
-
Event Lab
-

Understand publisher-listener decoupling through a visible event timeline.

-

- This page helps you observe what usually stays hidden: the request can finish quickly while listeners keep - reacting in the background. Publish multiple event types and then inspect the shared event history. -

+
+

+

-
Live Event Console
-

Publish events and inspect history

-

Use the form to publish events, then reload history to inspect type, user, detail, and timestamp.

+
+

+

- - - + + +
- Interpretation hint -

If the controller returns immediately but history keeps growing, you are seeing decoupled follow-up behavior in action.

+ +

-
Publish an event or load history to inspect live output.
+

         
+ diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index 39b4888..eda85bf 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -1,5 +1,5 @@ - + @@ -192,41 +192,39 @@
-
Spring Boot Learning Cockpit
-

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

-

- This homepage is now a guided study cockpit. Instead of only linking to pages, it explains the request - path, suggests experiment sequences, and lets you call real endpoints to observe how the demo behaves. -

+
+

+

-
Core labs4
-
Major layers6
-
Interactive pages4
-
Tested backend paths2+
+
5
+
6
+
4
+
2+
-
Start Here
-

Recommended study order

+
+

- 1. Learn endpoint binding -

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

+ +

- 2. Study users and validation -

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

+ +

- 3. Watch cross-cutting behavior -

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

+ +

@@ -235,77 +233,88 @@
-
Architecture
-

How one request travels through the demo

+
+

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

Call real endpoints without leaving the page

-

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

+
+

+

- - - - - + + + + + + +
+

- +
- +
- - + +
-
Endpoint output
-
Select an experiment above to load live output.
+
+

                 
-
Experiment 1
-

Trace validation

-

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

+
+

+

UserController UserService @@ -350,9 +359,9 @@
-
Experiment 2
-

Trace AOP timing

-

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

+
+

+

PerformanceAspect @Around @@ -361,9 +370,9 @@
-
Experiment 3
-

Trace event decoupling

-

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

+
+

+

Publisher Listener @@ -374,35 +383,263 @@
+ diff --git a/src/main/resources/static/learning-shell.js b/src/main/resources/static/learning-shell.js new file mode 100644 index 0000000..de74ec7 --- /dev/null +++ b/src/main/resources/static/learning-shell.js @@ -0,0 +1,329 @@ +(function () { + const STORAGE = { + token: "learning-demo-token", + username: "learning-demo-username", + language: "learning-demo-language" + }; + + const TEXT = { + zh: { + brand: "Spring \u5b66\u4e60\u5de5\u4f5c\u53f0", + home: "\u9996\u9875", + access: "\u767b\u5f55\u9875", + loginReady: "\u5df2\u767b\u5f55\uff0c\u53ef\u8bbf\u95ee\u53d7\u4fdd\u62a4\u5b9e\u9a8c", + loginMissing: "\u672a\u767b\u5f55\uff0c\u53d7\u4fdd\u62a4\u5b9e\u9a8c\u4f1a\u8fd4\u56de 401", + currentUser: "\u5f53\u524d\u7528\u6237", + logout: "\u9000\u51fa\u767b\u5f55", + login: "\u53bb\u767b\u5f55", + languageToggle: "EN", + unauthorized: "\u672a\u767b\u5f55\u6216\u4ee4\u724c\u5df2\u5931\u6548\uff0c\u8bf7\u5148\u6253\u5f00\u767b\u5f55\u9875\u5b8c\u6210\u6f14\u793a\u767b\u5f55\u3002", + requestFailed: "\u8bf7\u6c42\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u3002" + }, + en: { + brand: "Spring Learning Workspace", + home: "Home", + access: "Login", + loginReady: "Authenticated, protected labs are available", + loginMissing: "Not logged in, protected labs will return 401", + currentUser: "User", + logout: "Logout", + login: "Login", + languageToggle: "\u4e2d\u6587", + unauthorized: "Not logged in or token expired. Open the login page first.", + requestFailed: "Request failed. Please try again." + } + }; + + function normalizeLanguage(language) { + return language === "en" ? "en" : "zh"; + } + + function getLanguage() { + return normalizeLanguage(localStorage.getItem(STORAGE.language)); + } + + function setLanguage(language) { + const normalized = normalizeLanguage(language); + localStorage.setItem(STORAGE.language, normalized); + document.documentElement.lang = normalized === "zh" ? "zh-CN" : "en"; + window.dispatchEvent(new CustomEvent("learning-language-changed", { + detail: { language: normalized } + })); + } + + function t(key) { + const lang = getLanguage(); + return (TEXT[lang] && TEXT[lang][key]) || key; + } + + function getToken() { + return localStorage.getItem(STORAGE.token) || ""; + } + + function getUsername() { + return localStorage.getItem(STORAGE.username) || ""; + } + + function isLoggedIn() { + return Boolean(getToken()); + } + + function saveAuth(token, username) { + localStorage.setItem(STORAGE.token, token || ""); + localStorage.setItem(STORAGE.username, username || ""); + window.dispatchEvent(new CustomEvent("learning-auth-changed", { + detail: { loggedIn: isLoggedIn(), username: getUsername() } + })); + } + + function clearAuth() { + localStorage.removeItem(STORAGE.token); + localStorage.removeItem(STORAGE.username); + window.dispatchEvent(new CustomEvent("learning-auth-changed", { + detail: { loggedIn: false, username: "" } + })); + } + + function ensureStyle() { + if (document.getElementById("learning-shell-style")) { + return; + } + + const style = document.createElement("style"); + style.id = "learning-shell-style"; + style.textContent = [ + ".learning-shell {", + " max-width: 1380px;", + " margin: 18px auto 0;", + " padding: 0 24px;", + "}", + ".learning-shell-card {", + " display: flex;", + " justify-content: space-between;", + " align-items: center;", + " gap: 14px;", + " flex-wrap: wrap;", + " padding: 14px 18px;", + " border-radius: 22px;", + " border: 1px solid rgba(216, 228, 240, 0.9);", + " background: rgba(255, 255, 255, 0.88);", + " box-shadow: 0 12px 28px rgba(18, 32, 51, 0.08);", + " backdrop-filter: blur(10px);", + "}", + ".learning-shell-brand {", + " display: flex;", + " flex-direction: column;", + " gap: 4px;", + "}", + ".learning-shell-brand strong {", + " font-size: 15px;", + " color: #122033;", + "}", + ".learning-shell-brand span {", + " font-size: 13px;", + " color: #5d7288;", + "}", + ".learning-shell-actions {", + " display: flex;", + " align-items: center;", + " gap: 10px;", + " flex-wrap: wrap;", + "}", + ".learning-shell-link,", + ".learning-shell-button,", + ".learning-shell-badge {", + " display: inline-flex;", + " align-items: center;", + " justify-content: center;", + " min-height: 36px;", + " padding: 0 14px;", + " border-radius: 999px;", + " text-decoration: none;", + " font-size: 13px;", + " font-weight: 700;", + "}", + ".learning-shell-link {", + " color: #0f67b5;", + " background: rgba(15, 103, 181, 0.09);", + "}", + ".learning-shell-button {", + " border: 0;", + " cursor: pointer;", + " color: #fff;", + " background: linear-gradient(135deg, #177245, #35a465);", + "}", + ".learning-shell-button.alt {", + " color: #0f67b5;", + " background: rgba(15, 103, 181, 0.09);", + "}", + ".learning-shell-badge {", + " color: #33485e;", + " background: rgba(18, 32, 51, 0.06);", + "}", + "@media (max-width: 780px) {", + " .learning-shell {", + " padding: 0 14px;", + " }", + "}" + ].join("\n"); + document.head.appendChild(style); + } + + function mountShell(options) { + ensureStyle(); + + if (document.querySelector(".learning-shell")) { + if (options && typeof options.onLanguageChange === "function") { + options.onLanguageChange(getLanguage()); + } + return; + } + + const shell = document.createElement("div"); + shell.className = "learning-shell"; + shell.innerHTML = [ + '
', + '
', + ' ', + ' ', + "
", + '
', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + "
", + "
" + ].join(""); + + const firstPage = document.querySelector(".page"); + if (firstPage) { + document.body.insertBefore(shell, firstPage); + } else { + document.body.insertBefore(shell, document.body.firstChild); + } + + const els = { + brand: shell.querySelector('[data-role="brand"]'), + status: shell.querySelector('[data-role="status"]'), + home: shell.querySelector('[data-role="home"]'), + access: shell.querySelector('[data-role="access"]'), + user: shell.querySelector('[data-role="user"]'), + language: shell.querySelector('[data-role="language"]'), + login: shell.querySelector('[data-role="login"]'), + logout: shell.querySelector('[data-role="logout"]') + }; + + function render() { + const loggedIn = isLoggedIn(); + const username = getUsername(); + + els.brand.textContent = t("brand"); + els.status.textContent = loggedIn ? t("loginReady") : t("loginMissing"); + els.home.textContent = t("home"); + els.access.textContent = t("access"); + els.user.textContent = loggedIn ? t("currentUser") + ": " + username : t("loginMissing"); + els.language.textContent = t("languageToggle"); + els.login.textContent = t("login"); + els.logout.textContent = t("logout"); + els.login.style.display = loggedIn ? "none" : "inline-flex"; + els.logout.style.display = loggedIn ? "inline-flex" : "none"; + + if (options && typeof options.onLanguageChange === "function") { + options.onLanguageChange(getLanguage()); + } + if (options && typeof options.onAuthChange === "function") { + options.onAuthChange({ loggedIn: loggedIn, username: username }); + } + } + + els.language.addEventListener("click", function () { + setLanguage(getLanguage() === "zh" ? "en" : "zh"); + }); + + els.login.addEventListener("click", function () { + window.location.href = "/access.html"; + }); + + els.logout.addEventListener("click", function () { + clearAuth(); + render(); + }); + + window.addEventListener("learning-auth-changed", render); + window.addEventListener("learning-language-changed", render); + render(); + } + + async function fetchWithAuth(url, options) { + const requestOptions = options || {}; + const headers = new Headers(requestOptions.headers || {}); + const token = getToken(); + + if (token && !headers.has("Authorization")) { + headers.set("Authorization", "Bearer " + token); + } + + return fetch(url, Object.assign({}, requestOptions, { headers: headers })); + } + + async function requestJson(url, options) { + const response = await fetchWithAuth(url, options); + const contentType = response.headers.get("content-type") || ""; + const payload = contentType.includes("application/json") + ? await response.json() + : await response.text(); + + if (!response.ok) { + const error = new Error( + typeof payload === "object" && payload && payload.message + ? payload.message + : t("requestFailed") + ); + error.status = response.status; + error.payload = payload; + error.details = typeof payload === "object" && payload ? payload.data : null; + throw error; + } + + if ( + typeof payload === "object" && + payload && + Object.prototype.hasOwnProperty.call(payload, "code") && + payload.code !== 0 + ) { + const error = new Error(payload.message || t("requestFailed")); + error.status = payload.code; + error.payload = payload; + error.details = payload.data; + throw error; + } + + return payload; + } + + function describeError(error) { + if (error && Number(error.status) === 401) { + return t("unauthorized"); + } + return error && error.message ? error.message : t("requestFailed"); + } + + setLanguage(getLanguage()); + + window.learningShell = { + mountShell: mountShell, + getLanguage: getLanguage, + setLanguage: setLanguage, + getToken: getToken, + getUsername: getUsername, + isLoggedIn: isLoggedIn, + saveAuth: saveAuth, + clearAuth: clearAuth, + fetchWithAuth: fetchWithAuth, + requestJson: requestJson, + describeError: describeError + }; +})(); diff --git a/src/main/resources/static/users.html b/src/main/resources/static/users.html index 63e1452..7c4fbf4 100644 --- a/src/main/resources/static/users.html +++ b/src/main/resources/static/users.html @@ -1,5 +1,5 @@ - + @@ -212,57 +212,58 @@
- Spring Boot demo -

User management that feels production-ready.

-

This page now uses backend stats, keyword search, and clear API error handling instead of a static CRUD example.

+ +

+

- - - + + + +
-

Highlights

-

Duplicate emails return a `409`. Validation errors are displayed inline. Search matches both names and emails.

+

+

-
Total users0
-
Adults0
-
Under 300
-
Average age0.0
+
0
+
0
+
0
+
0.0
-

User directory

-

Search results stay in sync with the API.

+

+

- 0 users +
-

Search

-

Use a keyword to match names or emails.

+

+

- - + +
-

The frontend now explains server-side validation and duplicate email errors instead of silently failing.

+

@@ -272,195 +273,389 @@
+ diff --git a/src/test/java/com/example/demo/controller/AdvancedLabControllerTest.java b/src/test/java/com/example/demo/controller/AdvancedLabControllerTest.java new file mode 100644 index 0000000..f89a25d --- /dev/null +++ b/src/test/java/com/example/demo/controller/AdvancedLabControllerTest.java @@ -0,0 +1,66 @@ +package com.example.demo.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Map; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class AdvancedLabControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Test + void concurrencyLabShouldReflectSingleTaskBatch() throws Exception { + mockMvc.perform(get("/api/lab/concurrency/simulate") + .param("tasks", "5") + .param("poolSize", "2") + .header("Authorization", bearerToken())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.tasks").value(5)) + .andExpect(jsonPath("$.data.poolSize").value(2)) + .andExpect(jsonPath("$.data.finalCounter").value(5)); + } + + @Test + void advancedLabShouldRejectAnonymousRequests() throws Exception { + mockMvc.perform(get("/api/lab/reflection/routes")) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value(401)); + } + + private String bearerToken() throws Exception { + String loginReq = objectMapper.writeValueAsString(Map.of( + "username", "admin", + "password", "admin123" + )); + + String loginResp = mockMvc.perform(post("/api/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(loginReq)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andReturn() + .getResponse() + .getContentAsString(); + + String token = objectMapper.readTree(loginResp).path("data").path("token").asText(); + return "Bearer " + token; + } +} diff --git a/src/test/java/com/example/demo/controller/AuthFlowTest.java b/src/test/java/com/example/demo/controller/AuthFlowTest.java index 89e0882..8370e24 100644 --- a/src/test/java/com/example/demo/controller/AuthFlowTest.java +++ b/src/test/java/com/example/demo/controller/AuthFlowTest.java @@ -10,6 +10,8 @@ import org.springframework.test.web.servlet.MockMvc; import java.util.Map; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -34,6 +36,40 @@ class AuthFlowTest { @Test void shouldAccessSecureEndpointWithValidToken() throws Exception { + mockMvc.perform(get("/api/secure/me") + .header("Authorization", bearerToken())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.principal").value("admin")); + } + + @Test + void modeEndpointShouldDescribeProtectedRoutes() throws Exception { + mockMvc.perform(get("/api/auth/mode")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.mode").value("learning-jwt")) + .andExpect(jsonPath("$.data.protectedPaths").isArray()); + } + + @Test + void introspectShouldDecodeClaimsForValidToken() throws Exception { + mockMvc.perform(get("/api/auth/introspect") + .header("Authorization", bearerToken())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.subject").value("admin")) + .andExpect(jsonPath("$.data.claims.username").value("admin")); + } + + @Test + void shouldServeLearningShellWithoutToken() throws Exception { + mockMvc.perform(get("/learning-shell.js")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("window.learningShell"))); + } + + private String bearerToken() throws Exception { String loginReq = objectMapper.writeValueAsString(Map.of("username", "admin", "password", "admin123")); String loginResp = mockMvc.perform(post("/api/auth/login") @@ -41,14 +77,11 @@ class AuthFlowTest { .content(loginReq)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(0)) - .andReturn().getResponse().getContentAsString(); + .andReturn() + .getResponse() + .getContentAsString(); String token = objectMapper.readTree(loginResp).path("data").path("token").asText(); - - mockMvc.perform(get("/api/secure/me") - .header("Authorization", "Bearer " + token)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.code").value(0)) - .andExpect(jsonPath("$.data.principal").value("admin")); + return "Bearer " + token; } } diff --git a/src/test/java/com/example/demo/controller/UserControllerTest.java b/src/test/java/com/example/demo/controller/UserControllerTest.java index 3df39c7..0d23ec8 100644 --- a/src/test/java/com/example/demo/controller/UserControllerTest.java +++ b/src/test/java/com/example/demo/controller/UserControllerTest.java @@ -1,5 +1,6 @@ package com.example.demo.controller; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -19,9 +20,13 @@ class UserControllerTest { @Autowired private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + @Test void shouldListUsersWithApiResponseWrapper() throws Exception { - mockMvc.perform(get("/api/users")) + mockMvc.perform(get("/api/users") + .header("Authorization", bearerToken())) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(0)) .andExpect(jsonPath("$.data").isArray()); @@ -38,6 +43,7 @@ class UserControllerTest { """; mockMvc.perform(post("/api/users") + .header("Authorization", bearerToken()) .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isOk()) @@ -56,6 +62,7 @@ class UserControllerTest { """; mockMvc.perform(post("/api/users") + .header("Authorization", bearerToken()) .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isBadRequest()) @@ -73,6 +80,7 @@ class UserControllerTest { """; mockMvc.perform(post("/api/users") + .header("Authorization", bearerToken()) .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isConflict()) @@ -81,11 +89,38 @@ class UserControllerTest { @Test void shouldExposeUserStats() throws Exception { - mockMvc.perform(get("/api/users/stats")) + mockMvc.perform(get("/api/users/stats") + .header("Authorization", bearerToken())) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(0)) .andExpect(jsonPath("$.data.totalUsers").isNumber()) .andExpect(jsonPath("$.data.adults").isNumber()) .andExpect(jsonPath("$.data.averageAge").isNumber()); } + + @Test + void shouldRejectUserEndpointsWithoutToken() throws Exception { + mockMvc.perform(get("/api/users")) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value(401)); + } + + private String bearerToken() throws Exception { + String loginReq = objectMapper.writeValueAsString(java.util.Map.of( + "username", "admin", + "password", "admin123" + )); + + String loginResp = mockMvc.perform(post("/api/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(loginReq)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andReturn() + .getResponse() + .getContentAsString(); + + String token = objectMapper.readTree(loginResp).path("data").path("token").asText(); + return "Bearer " + token; + } } diff --git a/target/classes/com/example/demo/aop/LoggingAspect.class b/target/classes/com/example/demo/aop/LoggingAspect.class index 2436ce8a2c5051fd5b025bf955d9d574236dbd1f..38e113ffc24e976694574d2a38e03f7fb66a2648 100644 GIT binary patch delta 171 zcmaDX`B-v;Cc7X%8$%WsLpDPWJ3}rHgET|lWJ!)g%p42_lXcj2go+p$xD^zh_BTD7 zy1`0;k%OURau&M=W7*`%?3$t#j0}7V3NQ9gc{+RE(}q1@`Krkm*){lUAlmjcJ>9Vb xD7^VL$9ZPUOa?}Vd|2a}axvYMfmfq{VwsIHEofuV^(5&*USD?$JO delta 170 zcmaDX`B-v;COb0+L)PT~>^ed@j11fg3Qzl+o=x3grNGF+kT=;MY$voTa~F;p^Cu`^WjFi11hOtxY_ wwD~s2d1lLU21bTV1`wGICUe1LKA0>7lf_`Nl%bY^fq@ICu8yIBp@~5f0P5o`1poj5 diff --git a/target/classes/com/example/demo/aop/PerformanceAspect.class b/target/classes/com/example/demo/aop/PerformanceAspect.class index c9081f4e1dc346ff1305a1956dae5ac8d1c8699e..670aaffd4e2012e0f1e5c6e4368336979cf3b983 100644 GIT binary patch delta 787 zcmZ{h*-ukZ5XOI}+by@3Em0N`T2LcYP$L*4i7`YIq%RU46hcg_aBHfxC2c`X)Vks> z>O~Yq#2t4w5u2!qkNz`0850thY2np}Irp3SzM1*XxkJ7Mum1f{`U|j)^pt+cwcOp^ z7Y`=Gv8X}?6izc9kHu#OUB7IF`7TCv9x$fy&|sWLGdp#gsw&{HFi&;-JTrLC3xz$= z7m?oTK%gd2z)Nc)XLF|eway#fYP>Vp&U~k|3v&F>qPJBgHEiimM5(3BJyt zHIVl?RV@x{ltAKqioFH!!JYY32{T7*&*5(H^r1)=8i2{b~DT^ zVHK8Ci^TAvJ}LS|Zb-O{^*n69Db{GvXkTcP+a;uE&8)Dxb)L@+IWZMhaoKy4G9(uDsbR|iia2yF#4O_0_H5IbG0)O zkM&6;x0P}%u~f|qzD;kAl4cst%5V-Sm&9#JrgF$v=;wqQd4seD}`pq6<&ny;kM<@alT z3GdlP->gjRYo=}FOCHL2&(26K2eoWg0|VXqw*C`v^GF5(gl6j6uEtTo~aaVx|h z_BKg~m9SXCQ4@qj@g2nI(z{B}ERfR9`x-5l*-qMZKW^~q!cDy~{!^fsVoEsMcH~mA z6FcByaTj)>pW7M0?#M(AKAMYKbn`n*WP5RoqKtfYFe+I_FLa^;JI48O2cLY{;}^9MMkpGgDDP3VhwcOrTf=K2}mrQM??v)yyr0Pf=v-tn;~k@FcQ Mi7Jez5JU*JzvPst@&Et; diff --git a/target/classes/com/example/demo/aop/RateLimitAspect.class b/target/classes/com/example/demo/aop/RateLimitAspect.class index adbca1d44959bb95b437da5b384117aee3f8dcf2..25ba5adf086873c2bd8b1eb0f97e8811ff7bc482 100644 GIT binary patch delta 128 zcmWm3xedZF06@`iERoo;M4+Q`n}!ic!3NAh$sEwQ;U?j}Lu4jkhV-lgwC`26vd>aA z8+E3d8S{XJq0iC{^jn6k-Ag6^%{3dg0XxHU(A=?F7M!49Sr*Hy(igSwV@X{RMy7AMpSH delta 123 zcmWm2xedZV5J1syR>F8Z0uvH#N9k?Lig|D=wedUp< zF>!rqn}nJB#GU$Fh*>BDh6$^Yrp-o`jj&}GvRAqsToD!jJqc$nAy?(b-M^(0_*8qw Ut_c`(#4sge$petzJ<0L3z6}*0Z~y=R diff --git a/target/classes/com/example/demo/common/ApiResponse.class b/target/classes/com/example/demo/common/ApiResponse.class index b54b3a17eda6357f4e57f248a5a1507e2dbfd313..51106a523029bfc651dcc2a96a414c2a0be0fc60 100644 GIT binary patch delta 110 zcmca0aZ_T$RTlA3n-D9Xti-ZJ{hY+SbbbG%tkmQZYrDxaSuB}tLP93XvmO%UXJhc^ zVhCUeWM>HCVF+djnXJtAh?#>SZ1NLUz0IubI;`@33~US>4519+3=s^G3{ecx3=9lh O42%ph3~>wz43YrQa2c=w delta 100 zcmca9aY16kRTf5{$!}P!_i_@% diff --git a/target/classes/com/example/demo/controller/AdvancedLabController.class b/target/classes/com/example/demo/controller/AdvancedLabController.class new file mode 100644 index 0000000000000000000000000000000000000000..3120bdac6d3a6324cfaea03e138bacc5a624c870 GIT binary patch literal 14832 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 GHT@GlQ0OE8 literal 0 HcmV?d00001 diff --git a/target/classes/com/example/demo/controller/AopEventController$1.class b/target/classes/com/example/demo/controller/AopEventController$1.class new file mode 100644 index 0000000000000000000000000000000000000000..ea478f42da03cef301f7036404f7e4dd174f1d45 GIT binary patch literal 944 zcmb7CT~8B16g|_HZd(_r6`|l))kO*@3ncpDM{L?HHkM*+!3UnE+X*aAcbeTT>XZM) z7oXL{7-RSW{wU*}O=BZ{!OdpwoRj-;?w$MV_m5KmPq39k0!afY6H`btEV-(0i+8*~ zkizbXzOwtOC#1a>ioiJ$eq?pt4+KN{gv+4_8CJ_H2hDQ@J<1}3tbv?~3oscrCZb&B zM}d-31a?IY;&Ro$l(k7AR{O0Nxko#EU{SO+5^8CoRxH#yD>QM?$Wn_JP0S$AU{qf@ zm5x(mSU#s}J)nY#%b2BT74$rxONL~5rD0%>Vey!sa9eW!&~B;r&^@YqLiU^>P=T&n zG_erZ-FsQnbv1X@#I=~KIZbE8-7s-eb5qUT?MBPMEr$F!^KSQ8xDi9+u-_Fyhj%40g{E@3+~9>2J7{+Wmyoix|!moEpPL zg6T25PSA+qXQYtDEaq_qMXGU^>@v3Sfb2t5P$Qef64gt`RX>mi=!HREz#KBK4f5Fe k_n@5R*lPu=WK&q9^M1^%VV!J&G9+MQ^VtIRMqfVj2LNH?iU0rr literal 0 HcmV?d00001 diff --git a/target/classes/com/example/demo/controller/AopEventController.class b/target/classes/com/example/demo/controller/AopEventController.class index 57ddac5094c425cfa0371b11417af5ebdb7a69ad..3a0bccf5b9610ca55229fca52a781981fd9762d0 100644 GIT binary patch literal 9266 zcmd5?d3+T2b$?IS4$A;yG1vmz3|PiO2a9d6jfAm;xP-+K7H}{&_Gou@HLyFgo|%=v zN#h(%oU=#l98GhzZbM=>4t9f#?K)|jrmfR7PI{(y)BC>frb)l=H?y-Nt-$i<(?81R zvznRTyx;r1^ZVZKH+p$I43rcHUblC{p7m1^0_7OaY!&AU$Ex#hCuWk=lV?kUR& zM&lu-#WmmZa+Wt`=PjlU<4yDNTHa-kHRy(9(WN>qqfVx!masfrn<(4ixLw2bLqedS8`SomR10rPI}P4OmqzujqOe)5%*SepOkm zrYa>r>n7S8|dI+4N?G#4JWu{C|24ZN(E*(f(jnw#BLd_#Q=4x;j5G&ujHT&ybR&sZ`dB8nl6?qO;V?4XP|J^_iu_hrlU9C*!uyDZrDB$EaZ%3D>j^!-dXg}F3n zn1*N7d~>2aH}ETNp(ZRJK)CLQI}PPinNTg8h^uJ;jfJeRjH>6BJhNitO$X^>!1IrRCaXMZA;^mokjHuupH1cji76GYOb9}G| zq*%ziBG%|Pkd`L05=kg>G?-`v18=#P}!NKw_4ms;V>^`S3WReAM)XMGb*8?v_fR)@WLCssXu1#WH=YR6z;R=p55k zBeh`C_3Vd3T&U~%2IR6pY>agpeGhW%NIiTIsCGe3(dZGT>qe#ELk-UbXlI%)Zq;nXSM_cLuzfUGL5ap^TI6b;C36x@o541;gWcG1e8LmY*03{`%RX=qr@tCv}Xjwwka z5$x6i#6xK^fHgv(Mj$A2A@>WU%)04XIT1wf@>LBaxJw0sUc zKZYWQV2PzHic-SLm&B? z>(lfE{aA{AT&GXbd8QkiA_nMal^fkux6DFiR$z_P>+RWivboopZ_-d2ecWesdXk<( zLO^!6JY|L4hLT-ESlx#~=^3pL$ zRqT!$+HPrRyOk#0(vGUgjMQ&AiHi0F{iIHxm&%uNy@KtSnwKTConNB60KkhoEWP=z5!vbR=(MHyz=(t9|0Ekx3GorLUNc{PjGaRH4YkXZ@85AcI_PkOH3*6DZXcd@n~AYf2|*VM&Ry;(S# zg_L+Xl)*;t_jURM`a?8N0^F#3YiH3HYffIUF1shXL1Xtv^v5as6P^B){tPv=(b&!6 zjn=Lrt$w}9SF1O3OZJVk=Bsu7LZ`nJo1oVhtu^{h*6i|s7U!a8)aYMPHUwdZg@M|Bf!d7w zf7N~X$OPL*1A#)+$WLH)*kb`!|2?^XlUiCsHU3ZfuN3{aPT!Vl+}v`{vzJ4g1&_y) z)EV{DcXax$ib!-7l{ED;>)av(iv#x> zG@Tc58rB;>w0GmxeW|Esz}HrMxV~ojNb)43ji4vkwC1Q~+Qk)C$plBckus z=oQWm$tqXrypmTTKQUiWW2}(ai>P!|m?5Os=-j0&5|OG@0b)LfyIIihjr#T2N zS2u=Um9!3+^H9#)@?M56&bx5aIJEIN;(Zhy5R!Y`cC3S{Uq33FNJOEEfa9iT%X2i? zCOCnfqFLfA_8~A^=K%#ShfVA-E$p+vrCplAvk<{{imr&z0OHWpt7eOU#Eit)I-7KR zb7f47M3J=vLM+Om^aHy*=y&_9DuD$T%%$rH4jhmb$8BF)pT=1VblNiRdZnzdHWEcq zRoe3Gv^9~%&b5$j;BI!W6<|@>_d&1}+&HGJ(5_lf7)4H5fN{vt#2!xo6RUKfzr>~! zt*$zcL1xQnSmQ%B9a$A^Nb;yFP*s*`cQOkv*N!xBHOysnt8-n`nJ}KgV;uv&lxvwuJ7=CIYlsV@PV!chd%hp2G%NULJ}Z3 zaVmrgxH1t+acP${@yzM4%3f18D-#8?8|jhkAGR7r;xs54nDA`9k2x$ZWCEkjMKRr7i6Lo!@AdBnG}#8+LPAd zn4tyf)=nAuN`|r^hk716QN>IpW^FiKiW|Ec;@wWrS8>2h3^xcPKizricS-vQz2HT7*S0M)oGzB3zZ{O)tJhQ zr~;vy5~{O8^{`OAu}(GD1S3#oCrmI(+h{*MfUh8Lr`IP;@Mvs;N199^BXdo#RGEPJ zu9L(*594VhHpM#-I^z4yNmIP3Zi+W&-dex?Ubt`N485btZL5$pME67HQ;_)q1one) zo|%w$b1ZKU@0{Z@8M(5&qmXx3EbmK^>+(r?-&+^*-c;zi?-O(XfSCLJV(!Q5md<=I zBKWWnd_)L7Dg+;o2|js=ezZx&Ffxl!u>=(-X$v`Yi~=(8A|*V=_k6{X6V^XPht@N4EzKKB-?xN8^G@t{BD1d zE+U1W`(ozOd0Liv;XI`?KXsnkGhY~NhXa@N^>?*(wa?H`N9G&s=;}bS|9q%2?tf91 z`AYowwZ8cF>;3WVuYlF^8Tz%^sod2ef!wBYc@TI;^d{)=2;D(%raS2^2Amz6z8Hu__l4R=7KI&fY_?Bn{4@^D##G2YO)ulF z6_n?3#E^z9?#9QlH5h9L?*l5-I%0GPN4DWVEbIsWPU`qJZBEe>skXMZkrYlw1S^-c z(kr-p1HYGH-w;ObRxlztG>%`GLkIXEXs&6+z#-f;{xTo-c@YHtW*o6!f`|go2V$fB z7Sjv#dqmIEw_5P$&zZ&}cO2-?9MAk!Xy^XS-@y1utnAPHUF44b24t*9R1$2roE}3Q zegwGxQB;$U0X;uXBlHQ_f0SlM~t#K`{ySsW9pmG8zH2|DGHG4t@c?Nv)2Mdxt2W$)k1F`rsqqw_<%g;3e*s*s6CD5m literal 5818 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 Q85Ji+|HSvdZ2V{c19(w(FaQ7m diff --git a/target/classes/com/example/demo/controller/LearnController.class b/target/classes/com/example/demo/controller/LearnController.class index 8994ac41bc1de36478665bd468940bf29f4793e9..beaa941f7bef09f62276ea2dc946ab4303652489 100644 GIT binary patch literal 5852 zcmbVP=YJH}6}?Yw2O&TVB>`hIV4I>g;Lw6-LST?A37M5dWH9#A?%Umg*_mZ#RzfDk zPH~EpI3aO*@7=Kz$Hc}Z&J3;ZAe}Y{TC<=ADxkkog*iv|jMUrMzjS5q(j|>H3DQb1XFRm;LxO~<(H7WT z^_4dwXEb_1&}x%seqSjl+ZsIx{L-z2o7)AgGBJs_c0ih%RZb9YJ}jui45!J}N3kD* z9H>|t?S>#ToNBZeSY)NLin^&ML%ki;N2Z`|FlDNnvRtv1nO9}k%(+hBxwfr5bA;I% ztj%FF1dPeD5~Qb~W&{>`P&+ug^SZ$5n~#it6Pr<)M%(q zRE-FYo)EM#(k&~NDXY+7yG6^GtCJPFYeUrSi5YR@|uSy#^ zX5~>Ctirq@ImVzJ8WE{()ypZvujWby)JPoXD0DsK0RNyW`o8NxYf;eJ1nc=QM>Mh! za(rQ+2-zB47c>}c`G#8vlw;(ihje4VhJmea20cdL$sCtiD<9`p&bDx!!(D<030-C8 zbui4oDQQ>XLKm8lQ9ise1e`YnO+@=X64tIDxhl4_93##nBR&h>5|Zai3_Wo~St5H?Zz99$LI4}?lTo)Ho+bDQ$IZ32ioVSxl zbKK8vA`{ZxBCd{mr!;yFUF=j%&-t)scp80NP^s&6g4^2WgUP+bK>h2iFSot*|gbJs%` zA^`+#i3ysrd<*4sz;Rp<&NWWZff3g$nts?}3tVrxubMMTI=)$uxxn@2O|taO0CWQf!IFI?4!*2ZKV02o5#U*BbKAas^uBwO>hn82IS_Xon%M6 z*utGhQ18eG=xQ*h#lzbGzDZaP9~tQWyk$8B#O1oE-K91*FV;U_QLzmN7au*gv{9y3 zqUTQk320iCO?rT!!}XRlEM<9z%&8f3#&Ys$Uz|gHfpzZO{Qd}=tr-!~7WEN9hn5NC zl=Yk=MdkI!!I@%b!!oDv7C|kl+F-hKG;DieH4j^~pez5Es)l)4v#3J~+POHWYnCjE z?nBE)_i|*W+Y{(Sd$>`xJe6<8dzVn?@~R+#JdA&Yf026`UV{4&VBCrY88Q4Chf6ta z;le4%t~=Qv1$FWoyd~&h!yJse{xT62!E0Pf%p`YOPCtIR;gPdUaMS_9;i3P1s$tlj zwRnl2L`a6lnMn0**wq$2p3u=_p~?Jk!xCdwqQ(o6?RaK;yYbHq%k6IV*sh8b^R&WvZ0G|r+hgU!Cz*kyRSUQ^c zI}Ih|)0nNs?`QD$qEDZtb`0?KjQ^j*79SD#ZSA^C;&lv~==1o`+k_9>FW`Sibbwx> zFJdO>OY~)G4llooU)DBqk22GDY1K%#-mZ7NMQfYsQn$WQ-<%9TtZz+*+w_N$p`q_g zhL7mG;^AwM*BsiglQfLC;Qv`B<+$rjJK}di9Il$!MY{8BOUQi-+}gpM>3~;JE?#>@=(&q{rYJj;O9kM{GB= zQ8o-j9!=7>!M#R3^c{GyIpsxOxr3GOLc*#hTp=8$*s0%J#;M<@A7Fn+6A=CoGJo_G z{g{5@1LNZP&oKWv{UT=I6=>k@+Z8h~+5-ljiXisrS7HWw^eO#nytsgSGh`4A!TMp~ zege2h;IYvVvk^(HVeX<|hEb}~&Jbx+h%}02-ea#tdv{q_ z9PLf#=zPM}t?oDJb~2gLKe+;39_aExSAeowMcbTB(bZc^(_VUq-i2}vWta<6l%`QY m^GYh9-+@I@hk36S(C_0aCua^qn&@vh(L{d_ce%s8PLQbDCu#;EdyI-)9ZRqt00^`78Sn^D;gP1o$5jEZY&djcde zTJ8uXZ7r@GRIPz_H4&r$6-%^)QDBH$mfFuHTFR(C{rB55QT z8ipN8{o}9G-+eiA{?qiuOX<(=KYjSl5?V%;0a{)`GOb{=H0J~M;XIK9mTfg{`2d^d z?$plRl4up9(l}R5U80{bY8bHXgtc8!EC*EMsHPg?7f%`ks?n(FF+O79qIfFL0fA~H zBzhjK3e|NKUJ#lIO`5JC;-9-)1DYGOG4VMh#<7`oE(5KN}8-A{jUXXfs0H{KHMXC#?C%C!+5ljs1W+C0_0BJ{3i zUi>9$VN|3+?H4|eovJ<1qneu98|R*$KM4}Gqi#eQ@9F2dEzu!HFXy>D=q6<5@BPyHVjF0 zgweAzQ#aE$?oLmhou2$U{jWE)_!DSc|xHq29snpIGo7@_`3M?|m~Rjohm zwLA2KxE6KHHj3KXE?USL6xu!`2Ek=*n$A0tL%rPW7ReQT!-%T!OtQVr5_>=cs)n{7 z(To76#KZ)^6v>3j86;@(;*qBJ8N&ap?vDqnXZt^u;2ws}vqg8z?QgyvcceA#gu(K# z)~~BJ4kM$j`A7=3GJir&@?NDEBgCJJ11J(iT{K)q485|8Ml|1Vbshh^!k5rED*yQZ^9W)=6A zd5e3#So%91qKT$9oYYJnE5<7gF{+61J{8QJ@<$xflar}`-bU14LxG9$!YJhN&jxXB zP)lN}tul(|NfsD`JbMLdG1^>k5IPOZS5Zs6h`aO(x8j9Tbbds#B!Q03=%sn8BaaY- zTmH9H!L%#01>?g!I@&P@4g!3 z>}T!^kGgM!3G1!l{o!!Sp^lc<#d}s}T;bq)1bkb|2ArXhogwJH{xsx|ym^`hL&IC^ zYZ!A>7=?d~$%1pdz2NX?*?ALE#3*be%_x6E6T@g_-V1J{m=^drDA++4-jj0g$4#^j zFV{s*?3km=Ic%7 z6`&I|h&wbA8lqxH`3kPW+m;6uh)hs5(FM7*+ z&@6Vmt${zH7f7NPX$4i`KSZ_QvWco41gl*y9s-FPNhbqH63`Op+?`>pOzaeq<*{O=ekF8F)k+BE@oJI8q|N7LJt4KMhC9 zv8I6kayI(m>T4r8#1fi@?LM5OMnZs89;X;>APqx?MCtJae&sj zMm?Y~kB@CpSYow{P9co=w4>i5A;rFgh@D~VoQ7O^5mX4m8|2qL${Xa}@*6JE4LPE+L*@V=9|X}B z)Lg4WGUS@clB}ZloeIMzwnMGRq2}gHJQI0wpA7@7AX&<=ogsK3OYkBD-2(i;As7(H z?GgkT{gM9U(KtPq#z7yA)&*#E0>WX?=mL%K0yJ8jT+OE89vMN*2fif>{&E(4?(~f1 zz+Z8|f4CUe;Y@rKYbL)%I^~Gp( z`e<}5K%*BlqM#824ZZ-4!-di~yeN&2b2L72Y23)rxJhqeM>K&=C?#<=gAb@XBKJ(t zFQ;(lZ|Z{%O1G&8y@D4*Vs^Xy6juIBpW(O&t8aS+TAe#*$LjCXue_k(sHqFYXBhD@ z(flklKie0d>g-Xgrd#wmyh*gichuY(6+6Bfy#*+4hyNm5qhCT*9Iz;i6wzNCvbUY5 JVupQ1^j}|@cA@|P diff --git a/target/classes/com/example/demo/controller/UserController.class b/target/classes/com/example/demo/controller/UserController.class index f2794a8e854888e0a066c19d064360833f5910e7..834cdb96331eae074dbda8025fb02452c616411b 100644 GIT binary patch literal 4672 zcmcInX?N2`6usjlIEzTYYy}#cLV#=_TJ|PT5-2p_gphz+pqpY(91+_xk_@Ck_kH;T z{R92lp3??Sdrm+0Lw{6H??|$uAf<6>`#~Pf_|2R9?z{6w^T(gR{086@zHLVeO=&c1 zXuY!OthF&r%Mg8*ZEtz)(){U4I(>*QF)YqSDLo3?SXxGpIO<*8zm34W~C|65T zFUYc`=dFrsTcwh;^=po_&xT!rjv5DMq9};c8C%lWs$m6dAPXj1d`eGE+AcalIP$VjolJNW;$0lfGyG{yYW{OvMYn9xj)TzCIHchPyeQBUGp!RbEpOMP zLn$^_j52evxt>!m4?!5p+Yt>%m8%w6Hq4S5Ij-RZPLdI$$cbB4OdPMcvM6mbJBVSW z{}F+Uu^=NE4_5lyk-F3VTr0R|wyRChleeYe%0iD*%jc!z%+*SzMU|g38b&e3{P+d} zBYkl>EEBc{;Y!!%l&D`3xD<^ej@#ecN&XVF+lmYIKx)-OVj$xhF5(gc@eKsB(e*vC z{Hwq^F`;1+m$?G5(gg<-|6AgM7({vR&mX;%$bhK=tnCRi)G4W zkrX(Rwd|tqR8={eQxV;@>;?UFUUnk&XSplmkR2Nc`ggAC(8T+ zSqKF5Agbs1j{PSXo+htpy?#dQUYX%gn=4X%tl?A##oKekOtji8=B6zRP5l0VFL#v% z|BN{vC1JHQCXw`%NxnBk`1WQC(^{PQ8+Wn%hf)^N1 zC`?Zre+^b^eg`Ouz4LN+d_zC!lzOar2)g68k zR#(=i9hAp{VY`OTa$xc#W7*Pk>Xqqee9=JBV^(2NrO`j~{4`#VsV&axlNm_(0!&o+ zz5mPi+6igAuus(@`DphK;ZWhrcZX=d2EWWk! zy#>=8onh}hTlIhYz$1u(!Jm=-k-ZeI^0yPJux>&#KL*>d1=rZy>RTg+*VyAc&nrI@ z_};=2%tqikZg8q{f0RAJwqxKYq?fS%F*g0cuJ3=H_t$pMujZQtes6jox2lx&rd_WX*$>w*vt@Hx!q*dx3&kvXM+4K3m2 z$2hyJ4WCn;sypO?>+xNL;P&7i7P-t8s`OplXNUpJdm+pRp?n^4>^(kh5L?;2&qwjs z!K=8}`G6gP5AjiexXM=LzgHn1B(?*54)alnCo|_gDk+0k6CfWYq+<~o^oEdo13r~h z6!;V^yFBns^~@`dmjn1XTsN0>_mEx&jGgGgBe~ t`7levNJI%;A@Z)(l<--I@bd^l#cimB6u$6)ed(EI2;?jFzYfj6`3sdQ{sI61 delta 1306 zcmZ{i%TH556vn@~EwsJ8JS+lIL<%heLL9w!Li(F3z1fGv}W3n=jv2$7h@T?a$U10QFe> zD9;GZO$)rR8=ip6=7chchzh~iQO zPL%OI$!5$$g@Pnh@@!p)^#p^m^K^Mt8dzWZX)b_XOs=MC@Ahk3Qj6V;7$@jT>}9?+imBf5xozJmab??@8(A zkk=C&8Sf2wgCT~R68^zZZLMXnxFyj zu0ld9-%iPGzD5eCLPuW<^vHk-hjE>Lw3l!L-O;KZS{ZuLM=-6lZ=hQ-P;suoP_~YoO=PT4E+O7G zF9A%^z!dc3CRjK|)=z&$rVXgmQfW>P#jQcG5C$w`s$02X_fuAb4pNI@q_7 zH^7J?pQ4DOWCP`G7MCC^U&qN!oQadkDs>?;!(>FEn=R5q!?R-;9-5O$D&NBhl}8mE zrs4?asHTdS&c-NAU|F>IC@`2(O!h^bU%c1D_L%#MhpqOnRZQDvG+uT$ww5*2{1VlTLl?l2`n>HtMd>ey4r z2(hOelIUJ2pZK6j%?2%WE^Lq%wZR2T6FUIC5=OWZ+>i!$IO@~1D0@`pxErQ4H#|r{ z{KpN`nE8((n-4H#RRV%9(UEopX+{*muE`YA5aNcMjT>@Km1SQ2K2%FBB*r7k7c}C> Fe*rg~z-|Bl diff --git a/target/classes/com/example/demo/controller/auth/LearningAuthController.class b/target/classes/com/example/demo/controller/auth/LearningAuthController.class index 703dc36c1f7b013521485e9e0c0d0c2f07a3d965..5dce4674d68fe8194544daa6e9240c2921aeb398 100644 GIT binary patch delta 1670 zcmZuwTXPge6#jZQo0)7jw=o2S5?YhOB&%5kMNLXHXeeSORw|ni1B!HJJK2HB%rZTj zAc6}j-fy@nf)~8s@RAq^DJ?C0@xlM#U+~FC%kuQ>60yqK>h11xPJico-#PQg+CQ&P z{`U6|KLfZMmll&hvUP*roN%2S&&gSz+kwnUFLZ>fmVRexW(@R)p|JDijqy!J={Y&9VmQaW*zJ+jygH69gjLNW}DE6KyWqbmGef5!3nl$`bh$^C-#G}SOK zxya24&#tgl-1oTPh2oU!*nIa$wkL@h{c$pF%++BvmMaP)#^t-bBFAI_xN=i@?xyDJX+gP(s+kccnn8KP*t2#Q7M$WbFgdZfS3~*S9mkk z3j31Sf}<%M)7i~vB!t$e#IRvZC*!1%Q=Jt*;PP<6pp|9noZYW9mP+tOno>TX@q^YGRhl)~#J@vQ`FQu>(FEbqL z94gv4*1ZIx)ejxyco9qN%H#5d@i zpwEmphBxsRY9shxVMZX*YDh1mX_=B-)!@GEo_C|_-pVqm(U;@(C>s|0yNb^5k_S?c1#(>p^S8{0VK7aceX?S!0c5Cll80>}0L= zP<+h^H>;7m`-@C7WKs^ckemmb(n^EK)7(lyztYY9ZZhEMtDS`vQ!cmhpJi^@Mg= z$F;RY<|nPEs{zkg+pXtU@M5*lc+~sNL>%ZvGy16e+o==xAdP!*8_jti?#BTAhUoln zm$Iu}%HllrhS8g?S9K|gc(eHz)*EQsf2CLFDMy9(*Z;%&o2WM}jG(?H{mv@ Wr1SUmFovb*kwvEqbicSN*z!M~4}~)T delta 655 zcmY*VO=}ZT6g@9vn`x3JI;CmUR^vu75u$aWn=XP-6l^Je)mp#CnW>GoNt(>~i39~p z5d`iN>y$~Y|${+}n~ ztUyFJCRA(6Olw-s%9=$PqXNCEKAp*?+_ZSCPE>hr69gc*1`X}*_%D#%A%@bKTt#FR-Q^=;wd73S1wV5PJFy)H zu>)!JVhDX0MVv}~u3Fq{-c&Q7!=S(r`;KCQUamX3kYYVs|5?y^Gs*BFgBz@2@^3Px z#vzKBMiwY>9TDYQj*({s1?di$VL0Op6n4I6fO`nmF6E>$tc)n9lrze?2F^Ed{5`L| rfAT2%T*uB|kp9=P=cBmk!}NUYr4ftGx7-o8-DidP4)I-&LdLy6a~y?4 diff --git a/target/classes/com/example/demo/dto/UserRequest.class b/target/classes/com/example/demo/dto/UserRequest.class index 26858f5202356fe8238670430c59f5a54571c737..a6a7479b2432eb3e299a1197a0a606fb1f2ef855 100644 GIT binary patch literal 3055 zcmb7GZBr9h6n-vA2+&}{izsc;l!7KfT(FL|hH3$|VgW60ZM9!6$t|qx3*Fr~w7;M~ zrXT7|na=Q`KcGL#>2tHYuxly7%p~{ioadbLoO{mAKW~rz2Jj_b_Mr=zJhBDk&@E7U zC0|R!lFp8?rW&r-6zHBa9W%Hukf~I+deMX4Jo*aw00n_^!?g`{Ankoi8BJxoMl*1Y zO<#Fy>gSg71A(j~Z6z?ce1g0lc&4*c*H{+{C~BabvZZP1oq+-dF+^)}hiXF!wIwG| zJIbpM(({W2jNp<$PvC}V0(UCQoxG`66L66i8f~^hw zEiJRz-ZmOM3Os2#fp4t3!2%PvR~N|J%J>**>*X-JyUM%N>4Td<M_s(<9Y|EP3-Kg8G(B`u(%eZyL#qMTprae7F~FWi_4~?R$KPA@-`$d zlrBTJA+0UxnR*>yD%f?K&zW~yg^3?eC;s>-b$y2`r_^4%rz{Cv zOvak12jQk0M|7kWl4x<{0)6Xl%WJ5|rjE6g=tDO(m#opasg)L7H_)ZCAI9SsyYaLS z-(We9l>%1rOyKGX|6W*^M(9AKBG9`XqX@i7#=zU@?kOV_d?KBurTmFy*WGLF*UwC1 zq^OJMq4FD^xgWS*Qf1MSzMtHPuExs*>?zh$oiC!X2j2=@(CxY2?!I+)j3S!{zy17{ z1PV)zqrA3B<>&Doo5u+!r&x(&%;N`vD`$%`86hE0hU3 zH$t^(+~lV56f@eBTlg$o-PWr+ZOCb4h!cx*no{68t&%5h4tJ5o=M3sc!eu!Q*ZzX2 zO&ua%yK#vA+VmkxZ@AxuFZkO{9iR{Q_*)C*QO6ut)NC`T&xa!h^i0Q+;cE}{XD9$3 zft&2@{3_%}IK*7*d5X&sqD6;QJ&!pWVb0<>^*j}*-bhD!pSYVDh7cP{TRAyMFEfr`5%7gl{BzIs-vWvSL2Hza@ zwWBk>`kvTo?kgg|6;Y^fb>Xpf^KfkQ|L4Cc&Y zMs8}w^@7agWzo#!9W!&+me#!dyd-T$AZlnuDUh0}F)uilZmeV#*Wm=Z6j4kTHNBuD z-3g@7Lu=X!VLbuZwBg7VX=PLN{73?QctxPiF%oVBgfKo!?81SrZdTlTgwW>i_*3$`Ah9S_U5>1IwVENPam_MR~6 ztm?ePp@U15Jvb|!RWpA_v)DO~v=lCUW7VTjh4hZrDep?#PV-iRu?9*`nBm^^juars z=0`5jv0#?0oV=l{Sd)QPKcgC)ZQ#$1}XkYfN2y6#q)Qt^uO=euho0^d?NPBq3 zG}lV&*@l3kZXtz$hje5z?qOdE!@DwNWW<6};q zTFiA={21a`5Ey8z%FA#A#yAIS3)?Be*~-Dq^&oeLAn*pCAscUEn1W7-9@IoBHJd%% zUcNou5u9*AZ&g4?(NY7Aa9KS~sY&kP2lyW4Gj>qV9i&x~68;rB7&p;(k49L#&9nTXIk zpNkIrR8=0zLNqUNbn`=akrXN&<9w@7;^{Jjz#}gIn}LL@;$@(lU%pFq;75U9n}QD@ zOC!qqWxiFBwWUv%@%sPa`&^ys1+)=&g$}4>#{@SV`b_yn=3kL!xKkm;(vJ~u6j01# z9Her+!e&0ISr)v{dImb1HPEmdYL-U0^vi)!q%ct@s${c9n`|N44Al$w;i`+`edeeI zAK)tAMY^O$0zazUy)*$-|GNi`y1d8JX{LWCX(UN~HW52=3fFN1AL2Hp9hBa|JQmUa EFQv{(4gdfE diff --git a/target/classes/com/example/demo/dto/UserStatsResponse.class b/target/classes/com/example/demo/dto/UserStatsResponse.class new file mode 100644 index 0000000000000000000000000000000000000000..73320ee8cf4a7b47dbeb7a92a086bfafb3425844 GIT binary patch literal 1848 zcma)6U31$+6g`{Rmg6YIahlLJ&=6>gtw2Sgd^#y@6H4PTEz^`dz|+RwiPg$lBds&^ zKfyp}$S{)!egHp;;jE-!cj9*ZpuM|S_niB&_wGOcp8gGB8xJa&!K{OViy}$_)u-}U z`k{=D{Ku-J|3jkU zeIrfsSS14;B}$-Tw2`6xkqC&ErFp}}qU9*b?l3foRV}%wS=Ib7>ZF{$10Xb>h2LK#cqu$Ty${>mj%j3@0&P?js&*q?dig5Hrk_fGj!8(zUAU=Tp?#q zCcPcqrL)cHIrl8ZJ1#cxE|XI~4`s-lx2Gl;i|NtfQ`Ire#*YF{z13>%HVy;|SwxHN zAW{#9{X-T?9)^su*47;v9>_Sb=UiDZy@2kg_RbdgPMMzWelKI$S0=r%3-t+=N*iKN zV5L4eVxetFqXOs2q_9@H?(#gd4!(5p6`BI;BbV_oGC^PYnd=-PFn4&yBJg57`XD;i&y=4TK9Es2RLRx0*3X86 z=2#|qtBh%K;FISAwX2d&91M(($00kROp@^i|a9`lc>rHu8S-EhNV>`n^C~%_r zRB%_b+cq%#5lw}CMJ@3)_Do=gPVvcgn`wPNbKY`n)NHhWAmYv^(q8{$EQS&!#);I5hHoc?s_y*tN(timE BblCs^ literal 0 HcmV?d00001 diff --git a/target/classes/com/example/demo/dto/auth/LoginRequest.class b/target/classes/com/example/demo/dto/auth/LoginRequest.class index 5215006a42265c9b1ece2b74a1f4a08efe30bfd5..689c1cf5c79157baee7626d13df415222634c549 100644 GIT binary patch delta 70 zcmey(|C@h96|*2e8-ocKgDHa=urP2i a7&DkMm@(KfFfed2Ff!OPI5IdhNCE&4w+sXT diff --git a/target/classes/com/example/demo/event/UserEventListener.class b/target/classes/com/example/demo/event/UserEventListener.class index a549985756da22697bd849dab73ba1ea783ba3a5..aba6961bea96e3b58e8a48ba875ece0a3fa550bd 100644 GIT binary patch delta 1094 zcmZ`(OHUI~7(KT@A43BOiWab5ForiJF=0bYMAJkPqDWD|iaNHpWnemkGee=i>ihjz zOya^FJ2xgG1`;UrBg;>iro(liT82EOMb6}!%`Ks(3j+Zp z7@FqQyc%0_G(9$AS!zLcP%wdWVbtLK`bzaGQ$k`TB^DjcG}c@FS>K*|KMD%2<3<2C z!|1~;w=?v$7(|ib$Zj4JA|8d06 zvd~jzPH-W^rK%wA|6MS(BX2aL8JayuNFGiQ4;grJ%*<-Wq@|`7q$u8o>(W=sVhE2K zq}NGZwQXSs@Pw}5j@NKEs?@RPZokq~{gNT-4k}T%ptLnj(FlB)h98Xdk#@#?r?kf| zkYAw)P={(XAW6G0MM3+En5C8B5-!ucW@w*te<(v~S<_5ifW4^4KD5w!kUU-3@6|VZ z{kqHbKsT;Hr9M}wqK44{NYN)rq-kXk6@a%?T1CE?`=+*|*t>y_Eu026FtA2>66^rt zlp!fZ5g^+}-YEIoDRKxAaIzg3z+sOHr`!l3Wsvnq#VVv?Wm0q0BLLrY0CE2=hDB=a zBZ>@~jd?6kCWBt#dWwQlxQ35tC^0=#W-6urBWx$N>DngDZ(*gx{}iT{U?gghU~{w@ zFw6Rnlp#X|**Na0vJU zk delta 1060 zcmZ`&+fEZv6kVqUI-NEtDWFymD-tQFpizNf2oTe#2>}IEprTeBu~3S%r3zjOc&mVj z84TeOUqqt_LW*gKeQnlM9y!yFnI z_KO?ZoN_5LxjH#3O}vptX4Q;u?}v0~V#w%p2Am~*ewVwX#_M(V*SdUu9*sC{L@LfK zHl)lj4HFiBq;_b690nN59QJ_V@ymIFNAQw{$*>ffUx|#8h6*dQ?zIwCEVQiJv8nL- z)DqboZvL%b)tr2a)#$X6e48QPQQOnu@-%s!?Oig_uC=*&>BCbMeRVoYHl>=ke{arumvf|##yqA6a~`JhI8~~IFAd^qLu7k@jG7{lxwyU29O5> zcEW^w+UH>#3Sgs0#@M7_7RE7vLO5}eu-Yk>%4h>zqE{9o(3jzIf<&ZJE%|J3k-j^O zya+0QFp4u#`3_ajA-*&ig62B%EKr>ZStQ*~6|$^c=xE(X-UH+>rN}{8a0uH`M&im9 zX;#X$5OW8vC?ahMk+!%<7h&k2Y1YA>_NQVK)dLs-Q4#5i_fyJ$?7wix3i_3B0Q(`D*Y q(gTS(U!(nXdNT<+NC5rHdP7-n;T}cg|M@;PBUEj{LyY1PEWZF_%G?$J diff --git a/target/classes/com/example/demo/event/UserEventPublisher.class b/target/classes/com/example/demo/event/UserEventPublisher.class index 18c2f4574fadc271e4240364e91b8f1714336b51..5847fa43b06a72f906c8ee2cfeb84854d9e2de23 100644 GIT binary patch delta 864 zcmaix&u5yCucxN4(KtD2lvxNPJZ$#S6s}juwmuk-$Y0OGq(j*_EYC zE`SWfxJPiBwR)r9pI3NAq||TXu{Il0JKHVJ;IHsbsqTmwZBD$_o>-%PY#Jz|qJx__ zh^naiKc)lNVmMsz9OV0sczeShgf^GXY-U%RZJRNenMQ-R7oC#pa+g6}C6W$DEa)-T z5RRVQyG)Pf2g7CBHQF^V%Zd+r{(c+Gu1O-sU!u7K-ZuPY34Qx z12Z}5EGIH!Q}I_g8=l+6!tXxzVFXd+dL*;^;-L?7=))ZNkV6N3LngzWf3VWwOMhYQ yC)NRP=08w{|FKkT5Zs3%e*N*gl;5KjpqWdwYN%5dCQX9=2{1Hrv{{fBX&G@lm<>K9Y&zp14J@?Iw`(cCq_2*(4Ko&RV z?eE&U7@E-%MXQ5-Xp=LMvHEraxAJyw<@Ic3x>)%%>py;0nSS9vd2b_*4hIIZ@`qN4 z9dxi8odON7B6aCZBA@m2y7qex?i|;ci`Um&B;3xh1;W^|NN!@`O)&?)8*1ayC=M~ClV&cY+S^pC@wqL zigEePT$1JJCjAjDSr%x($s;g8t6b60YI5t)1{y?_J%T!oYBkiZUXL?e2q*pmu7|BiBT3a10h zE(URi6B1Nc<0HZNm_`ElNm&?L%fcaAIJ%~VCTh0O!o${J+yAhj<1CG6IEO#~Xqfms bk41Ze(wBlz-0nXNDz^6PZ}@aP%hiBSt7SGRqz&pB5a bZGXAY!5TC6V;x;i>M?E$CN6k`YYY7kISz79 literal 0 HcmV?d00001 diff --git a/target/classes/com/example/demo/exception/GlobalExceptionHandler.class b/target/classes/com/example/demo/exception/GlobalExceptionHandler.class index f7eaab4298d8bb6ad9f27139e2c65425559e1655..3959f3a11562c47b75d31fe2aab09d4b031fcef6 100644 GIT binary patch delta 1284 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 delta 632 zcmZ`%OD{t~6#k~oy{&t1DzWg2#T621yC7mA5(^0pQH_O_NDvlY^(yLJ^`_pV^{W3s zB&da0`w@PF6+4`nUMdocIdjgL^L^hrGjmxO%h#Si9v%VIB6+R(6-`44`V98tDRT zBPP#V5TJ|zS5vj9b9s1=mQ$7i=Klh^EFi-Ub_uY-lUh65+KQJ&+NthkDFx5A4{x&&ouszwL8zX{`# z&~0_~q8CLn6I93va{nQ-IuZmnbfSx_xQ{m4g}QleRN3RA+JXLv&I qmBeMlsw^>|1pl;dTCM+5v*Sc!PvyIcS30BkoR&s@D}AmD6np`|;#~s( diff --git a/target/classes/com/example/demo/security/LearningSecurityConfig.class b/target/classes/com/example/demo/security/LearningSecurityConfig.class index bd649571da5e49ea4a5f6a1efeff0c1626952bd6..e6fcd816d16b6d87c5dab57f1123dfb340c16eb7 100644 GIT binary patch delta 1029 zcmYk4+jA5}6vltOncdkP#=WUj8Bs`47MN5q4^ozw2nZ;m5tKE+28m2|+iaF)5_V=+ zE}D41pnx*oFDQz}yTnxG!744D^Dp?9_+oi_+$ayzea_e4`T9F&`rL-A8=U38fBOTt zk1Gq#<-R%B^Jk_!zY{gQ!NDw>6-CdlMIAqyZhH;wx1vUtI~A+Esi3Y;Y-&WuT3PN= zNMWs}!?1_kV@z9VUHmII-&ZPRIO}kZ^EPk0yu*dX zOd+MbBJV10+&I$oyS{h}CcV8AOE(y!@FZsRo1CQfAFJtucawPFr(gTHckiP|-a z#yRv~qmUEk^MYBtAZv;jlMdHN^pfN+%cNg9gbI0;*Fs+BjWN#frkt#klegrHd5=$f k(M2xxqAwZvns0>GFO==f^BpC5(er#S27cfte&MEn0QZyBh5!Hn delta 1022 zcmYjPSx*yD6#i~on06TGv?7ru5d#UNL~DXE#vr07f*>Llu|?6b9Vk);=u8oCskg4U&UerK&Ueq5`J#~`@%!&D-vDgG$7ylg zo^<(RsuI`yKA#ht7zDr4-D3rt8PfeqPlrFDt8v|ewlJhwP*hc7M%0O|43Y&X2_tHy zlvzmFNGL{k+(OFvr4B8k_HT$9T`?yrIAD1sgjaYy^E2JXR#TU1G);|H z#uQywbphiHtyH1aoRGJ(Df5@?WA9DbQyEcVLmSP!igp|AqHhk&SMnLcG(|SFlg)sg ztQQWff(x05Qk1orjSifoi0&Z`5*RwkPoXK&MiW36U1~GuJY`8vl!H{VYsh93X%3O( zVl4{jD_lWZvP@b64c)}kL$@5X_x_{T2+(Pz5@-Oj$lF0af5{Y5$OAN=EPX{u(C%Fy z2s+HT*Pjf^qIX9iD5rTV1GCsYkJ?%6b1WtuSVqV>w0P9~zXw^w3ampl@=#BrL&&EQ zud`(HcT+u_LcgVjX0@cHl)A*vT)s~T&L(ke1tZtaTf)1!$l$_hoT12pvt(^JXT33x zyd=fvX$luwz;s;1B^{S>C4{TEMko1n@{HDX+`!!>&&0ze?-8Ej4Q1sZ+fHL1!vs9^ Pw~yg137Et?d_eYJYFoz( diff --git a/target/classes/com/example/demo/service/UserService.class b/target/classes/com/example/demo/service/UserService.class index 186beb256a960acc3bfe5561f670752e3f4b4f86..d667423d8ed323d7908534bffa6d74b7c5b015c2 100644 GIT binary patch literal 8542 zcmb_h33wFed49i@#7e6H#2RD-SS&(dNgN9pgKdG0BoGz|a1a&-J29i(ku+$vBhStX zl(bEo#!b>IM=#^1N1Qfp)5}d`aka5~C8_J)SJJ!gk>2;2#&O?oW_EX^U4-*IHGX#f z|DS)p^Zkzh*DGJX@hJcY<#GZs)WuP6paIJSEf>vMGo3T@6X{c97p;sZSa!h9+unnM zx~}fgI2r}r{-&a5=h7J`pDDVomG{yE&dl7Y{8`ubtUlK@=Z5Wqmp~kKO=yM@$8rNJ zuu{;Oai-JOWpjEaXQi{&w39Aa?yQ}$(vNa55{v~6Ma~M8Xt=^nN%qFEO0c!E7!kqr zoM}6g?(-kRPJSYR7Tlq#trpaG9qS%dSy~O;iM4`eJA2g1TQ0wH1ziz7OOYi1b%OO( zN=!RhE9a}AOg9*~3+;lH6PDMP&kp4?t~E_7`BprZKnk1U=rFJuoq`n=qpQ$@_^g>L zTBpVZcTng+FyANoy0;Nq1&O{KO_my%wDOJU609ue!LiUT74{KvMm*Q1KufNah<1-QV-N0$W3PdGv9F}H zk0SLeEn{Z0f;CZw0S0@!&o|;e>fG;|`K*&q4cm=)fW{dM=25L1aex~KOxMY!&JxCr zAaYk_bRONNvK%tdhkgRQ;JKDLO{aE+*6@W2R@2mxFME>=m%|2*V36*ib!SZ160BYD z#el!8aO!Qw!x)O=n1SOM7Tj5Z>jJXM+;KgNdhd<`Fhc3pA|-GVr{Z|Tz*}&d(pT{q zaB?|@u%ltL%yR-Ow=WPml%~AX7yOKYM=|Q_=@Ss_iV9ctO%y|+$C3~{X5bvoGw?i1 z-CLqjK~hw;@nSyX*-k$Fh-+o-jOkgvC{Gx8E8a%&$L*Y_ki9=DNsW)9G#8lKCwIYs zi7`RbxSh`q+OEpgy1*c(W>g9@M~BCdHDG~JSjQ+9q@&36BQeUXEXzo7W+rD_F2iUN zb{rQCOd%&oR{T?9!W`tdM{))0%%tmFT7-dWf;H@%KpsvUGX|altrJP#BYtesiFTo% zCZF;%&-n!NpJ$+mS-w#t^yO5v1t##btP;L#U=D9r2f8-HV|8hl@4em&eZ%=lhd zp(@Een(RUhEVWyie5--?;{#Md?cF~ *%lrKQMdy84iTZ^O6K8wFoXR^uUE{!Yc{ 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 delta 2313 zcmZuyYgm(I7{1?a>>J}_j1AC069-jBU@8`Ln_uIL`rcC{b zD;@hIlin!K0^ zD3LHx#w6Uvy=p7ps~Mx4fZJuVhwt=XtYIVvPn%5Y&0G4reHUi^}}KY`Dc;l?~}*W^3&u)rgu z2403JpUYod$zWD;@=)eECAHov%0`bE%$KnMb=)V7w)$n%V`6?jCHcMZ47J&aRL&|sW6^q7pbc$}M~b9xHd>tsBECmEzFmw%G8hGZn9^HaLS zl=avkVWW&q*h~%w?Fpt*>aTNqtN2p+cyDQr+gmx>;~{_3Ga?);ar^wdAhz6~7XgKf zL%4ROuqLa<8zeG?jzpl2U?@_e;`lJbT7Jebt&q7HSTRFvsoz;qJ<&OTinH89I<=K9 zkIPR3sm@hXyU;~7XEPY$JkFZ(N~b-vYahfgke@f?q|z3vCtH;5urj-YOi$;3CYX6s ze6A*!VLV?Z+4-adJJ*`bJlnLSrx8Qqv^XLBJ1OHB4jJ(QgDss;Hm0hLIK|@ptEgF*RJ(ln|BPixhwM- z1%4$0zm{!w9r#9ftoIiAuAX`WdXcE0dzYQuzB=W(Sd#fs2zn2Ssl0qXveK8dQ1+^rvR;( zb`+K{O%f#qSV?{g2~VXz8`5YlXp=&S3N2w{J9ZH3i?kPrVl)#8y@Z!35s}brMT0F; z!%Rg(8=UQ^Qjy_~FuN!nU?=u8V(&-n_6TNMH?s{pu}fhl;+@2gkU_7|>Z{mI%tApK zDG^mj&LRnktw?S|YAdRRo~(A{s!)~{CO1$pkO>2_sLt6D#L3;n$=HLvVd6b>fMFl@ zBM>4yP8cGE(#)(@)Rtw_vsyjU8Y+N~5zYWHwzi`|q;6?NBZb_z0rA1*>~<_uqbz&( zay8q2_2faUm`=8>1P);J5rS0*TcHnhG#*GTkxOmpKrakJAQyw_m*^(yHwVUIXi$A7 zX^ExrYh<&?t+YFjQpGy4CNfeH1E7ccGGbZo-_!)c)M!=E(@>Ue@G4bZOke`8Z?KLX zXeLVocvOY_-Ww5rt!QyJq&nnn9$kxk@^&QppdcvH7Q*V5)>n~cq|4Ke4n-QZ|7G+E z^%^-&twz5ubknK)!sz?}#R;dQsho|OR8r~*T4b>Syr(dXq~ ztOzJhP*43AD(xC5(TSq}BCzNqreF=F$e zMb}L(1r6Z~`oHX`3*ICRD!hfa=`A?lq4#n6A4pDrLmJ=3dw4%M`Vb%C6J_l)TI;0f Vv&#HSn*U6v^tAR1e#0eL{sAZ%-l_lq diff --git a/target/classes/static/access.html b/target/classes/static/access.html new file mode 100644 index 0000000..2101683 --- /dev/null +++ b/target/classes/static/access.html @@ -0,0 +1,389 @@ + + + + + + Spring Learning Login + + + +
+
+
+

+

+
+ + + + +
+
+ +
+
+
+

+

+ +
+ + +
+
+ + +
+ +
+ + + +
+ +
+

+        
+ + +
+
+ + + + + diff --git a/target/classes/static/aop.html b/target/classes/static/aop.html index 89167ea..6e34579 100644 --- a/target/classes/static/aop.html +++ b/target/classes/static/aop.html @@ -3,220 +3,347 @@ - AOP 切面编程 - Spring Boot + Spring AOP Lab - - -

🔪 AOP 切面编程

- -
-

🧪 实验任务卡(AOP)

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

📊 实时性能统计

-

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

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

📚 AOP 核心概念

- -
-

1. 什么是 AOP?

-

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

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

+

+
+ + + +
+
+ +
+ + +
+
+

+

+ +
+ + + + +
+ +
+
+ +

+
+
+ +

+
+
+ +

+        
- -
-

2. 核心术语

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

3. 五种通知类型

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

💻 代码示例

- -
-

日志切面示例

-
@Aspect
-@Component
-public class LoggingAspect {
-    
-    // 切入点:匹配所有 Controller 方法
-    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
-    public void controllerMethods() {}
-    
-    // 前置通知
-    @Before("controllerMethods()")
-    public void logBefore(JoinPoint jp) {
-        System.out.println("[AOP-Before] 方法开始: " + jp.getSignature().getName());
-    }
-    
-    // 返回通知
-    @AfterReturning(pointcut = "controllerMethods()", returning = "result")
-    public void logAfterReturning(JoinPoint jp, Object result) {
-        System.out.println("[AOP-AfterReturning] 返回值: " + result);
-    }
-}
-
- -
-

性能监控切面 (@Around)

-
@Aspect
-@Component
-public class PerformanceAspect {
-    
-    @Around("execution(* com.example.demo..*.*(..))")
-    public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
-        long start = System.currentTimeMillis();
-        
-        try {
-            Object result = pjp.proceed(); // 执行目标方法
-            long duration = System.currentTimeMillis() - start;
-            System.out.println("[AOP] " + pjp.getSignature() + " 耗时: " + duration + "ms");
-            return result;
-        } catch (Throwable e) {
-            System.out.println("[AOP] 方法异常: " + e.getMessage());
-            throw e;
+
+ + + + const box = document.getElementById("resultBox"); + if (!box.dataset.state || box.dataset.state === "idle") { + box.textContent = text.placeholder; + box.dataset.state = "idle"; + } + } + + async function renderRequest(path, options) { + const box = document.getElementById("resultBox"); + const text = pageText(); + box.textContent = text.loadingPrefix + path + " ..."; + box.dataset.state = "loading"; + + try { + const data = await window.learningShell.requestJson(path, options || {}); + box.textContent = JSON.stringify(data, null, 2); + box.dataset.state = "live"; + } catch (error) { + box.textContent = text.requestFailedPrefix + window.learningShell.describeError(error); + box.dataset.state = "live"; + } + } + + async function loadUsers() { + await renderRequest("/api/users"); + } + + async function loadUserStats() { + await renderRequest("/api/users/stats"); + } + + async function loadAopStats() { + await renderRequest("/aop/aop/stats"); + } + + async function sendInvalidUser() { + await renderRequest("/api/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: "", email: "bad", age: 999 }) + }); + } + + window.learningShell.mountShell({ onLanguageChange: renderLanguage }); + renderLanguage(); + - \ No newline at end of file + diff --git a/target/classes/static/events.html b/target/classes/static/events.html index 60f8858..d853654 100644 --- a/target/classes/static/events.html +++ b/target/classes/static/events.html @@ -3,251 +3,357 @@ - 事件机制 - Spring Boot + Spring Event Lab - - -

📡 Spring 事件机制

- -
-

🧪 实验任务卡(事件)

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

🎉 事件发布演示

-

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

-
- - - - +
+
+
+

+

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

🔄 事件机制流程

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

💻 代码实现

- -
-

1. 定义事件

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

2. 发布事件

-
@Component
-public class UserEventPublisher {
-    
-    @Autowired
-    private ApplicationEventPublisher eventPublisher;
-    
-    public void publishUserLogin(Long userId, String userName) {
-        UserEvent event = new UserEvent(
-            UserEvent.Type.LOGIN,
-            userId,
-            userName,
-            "用户登录成功"
-        );
-        eventPublisher.publishEvent(event);
-    }
-}
-
- -
-

3. 监听事件

-
@Component
-public class UserEventListener {
-    
-    // 基础监听
-    @EventListener
-    public void handleUserEvent(UserEvent event) {
-        System.out.println("收到事件: " + event.getType());
-    }
-    
-    // 条件监听 - 只处理登录事件
-    @EventListener(condition = "#event.type == T(com.example.demo.model.UserEvent$Type).LOGIN")
-    public void handleLogin(UserEvent event) {
-        System.out.println("用户登录: " + event.getUserName());
-    }
-    
-    // 异步监听 - 不阻塞主流程
-    @Async
-    @EventListener
-    public void sendWelcomeEmail(UserEvent event) {
-        // 发送邮件...
-    }
-}
-
- -
-

4. 控制器中使用

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

🎯 应用场景

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

💡 最佳实践

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

← 返回学习中心

- - + + const box = document.getElementById("resultBox"); + if (!box.dataset.state || box.dataset.state === "idle") { + box.textContent = text.placeholder; + box.dataset.state = "idle"; + } + } + + async function renderRequest(path, options) { + const box = document.getElementById("resultBox"); + const text = pageText(); + box.textContent = text.loadingPrefix + path + " ..."; + box.dataset.state = "loading"; + + try { + const data = await window.learningShell.requestJson(path, options || {}); + box.textContent = JSON.stringify(data, null, 2); + box.dataset.state = "live"; + } catch (error) { + box.textContent = text.requestFailedPrefix + window.learningShell.describeError(error); + box.dataset.state = "live"; + } + } + + async function publishEvent() { + const type = document.getElementById("eventType").value; + const userId = document.getElementById("userId").value.trim() || "21"; + const userName = document.getElementById("userName").value.trim() || "observer-demo"; + const params = new URLSearchParams({ userId: userId, userName: userName, eventType: type }); + await renderRequest("/aop/event/publish?" + params.toString(), { method: "POST" }); + } + + async function loadHistory() { + await renderRequest("/aop/event/history"); + } + + async function loadInfo() { + await renderRequest("/aop/event"); + } + + window.learningShell.mountShell({ onLanguageChange: renderLanguage }); + renderLanguage(); + - \ No newline at end of file + diff --git a/target/classes/static/index.html b/target/classes/static/index.html index e5e1b38..eda85bf 100644 --- a/target/classes/static/index.html +++ b/target/classes/static/index.html @@ -3,223 +3,643 @@ - Spring Boot 学习中心 + Spring Boot Learning Cockpit -

🍃 Spring Boot 学习中心

- - - -
-

📚 学习模块

-
- -

👥 用户管理

-

RESTful API 设计、CRUD 操作、参数绑定

-
- -

🔪 AOP 切面编程

-

日志记录、性能监控、限流控制

-
- -

📡 事件机制

-

发布/订阅模式、解耦业务逻辑

-
- -

🔐 鉴权演示(学习用)

-

最小 JWT 流程:登录、携带 Token、访问受保护接口

-
+
+
+
+
+
+

+

+
+ + + + + +
+
+
5
+
6
+
4
+
2+
+
+
+
+
+
+

+
+
+ +

+
+
+ +

+
+
+ +

+
+
+
+
-
- -

🔗 快速链接

- - -

🧪 接口测试

- -
-

GET 参数示例

-
- - - -
-
-

GET /learn/params?name=xxx&age=18

-
- -
-

路径变量示例

-
- - -
-
-

GET /learn/path/{id}

-
- -
-

POST JSON 示例

-
- - -
-
-

POST /learn/body

-
- -

📖 学习路径

- -
-

1. IOC 容器

-
    -
  • @Component, @Service, @Repository, @Controller
  • -
  • @Autowired 依赖注入
  • -
  • @Configuration + @Bean 配置类
  • -
-
- -
-

2. Web 开发

-
    -
  • @RestController = @Controller + @ResponseBody
  • -
  • @RequestMapping, @GetMapping, @PostMapping
  • -
  • @PathVariable, @RequestParam, @RequestBody
  • -
-
- -
-

3. AOP 切面编程

-
@Aspect
-@Component
-public class LoggingAspect {
-    @Before("execution(* com.example.*.*(..))")
-    public void logBefore(JoinPoint jp) {
-        System.out.println("方法调用: " + jp.getSignature());
-    }
-}
-
- -
-

4. 事件机制

-
// 发布事件
-@Autowired
-ApplicationEventPublisher publisher;
-publisher.publishEvent(new UserEvent(...));
+    
 
-// 监听事件
-@EventListener
-public void onEvent(UserEvent event) {
-    // 处理事件
-}
+
+
+

+
+
+
+
+
+
+
+
+
+ +
+ + +
+
+
+

+

+ +
+ + + + + + + +
+

+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+ +
+
+

+                
+
+ +
+
+

+

+
+ UserController + UserService + GlobalExceptionHandler +
+
+ +
+
+

+

+
+ PerformanceAspect + @Around + Cross-cutting +
+
+ +
+
+

+

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

📁 项目结构

-
-
├── 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 配置
-
- - - - + + } + + async function publishEvent() { + const type = document.getElementById("eventType").value; + const userId = document.getElementById("eventUserId").value.trim() || "99"; + const userName = document.getElementById("eventUserName").value.trim() || "learning-user"; + const params = new URLSearchParams({ + userId: userId, + userName: userName, + eventType: type + }); + await loadEndpoint("/aop/event/publish?" + params.toString(), { method: "POST" }); + } + + window.learningShell.mountShell({ onLanguageChange: renderLanguage }); + renderLanguage(); + - \ No newline at end of file + diff --git a/target/classes/static/learning-shell.js b/target/classes/static/learning-shell.js new file mode 100644 index 0000000..de74ec7 --- /dev/null +++ b/target/classes/static/learning-shell.js @@ -0,0 +1,329 @@ +(function () { + const STORAGE = { + token: "learning-demo-token", + username: "learning-demo-username", + language: "learning-demo-language" + }; + + const TEXT = { + zh: { + brand: "Spring \u5b66\u4e60\u5de5\u4f5c\u53f0", + home: "\u9996\u9875", + access: "\u767b\u5f55\u9875", + loginReady: "\u5df2\u767b\u5f55\uff0c\u53ef\u8bbf\u95ee\u53d7\u4fdd\u62a4\u5b9e\u9a8c", + loginMissing: "\u672a\u767b\u5f55\uff0c\u53d7\u4fdd\u62a4\u5b9e\u9a8c\u4f1a\u8fd4\u56de 401", + currentUser: "\u5f53\u524d\u7528\u6237", + logout: "\u9000\u51fa\u767b\u5f55", + login: "\u53bb\u767b\u5f55", + languageToggle: "EN", + unauthorized: "\u672a\u767b\u5f55\u6216\u4ee4\u724c\u5df2\u5931\u6548\uff0c\u8bf7\u5148\u6253\u5f00\u767b\u5f55\u9875\u5b8c\u6210\u6f14\u793a\u767b\u5f55\u3002", + requestFailed: "\u8bf7\u6c42\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u3002" + }, + en: { + brand: "Spring Learning Workspace", + home: "Home", + access: "Login", + loginReady: "Authenticated, protected labs are available", + loginMissing: "Not logged in, protected labs will return 401", + currentUser: "User", + logout: "Logout", + login: "Login", + languageToggle: "\u4e2d\u6587", + unauthorized: "Not logged in or token expired. Open the login page first.", + requestFailed: "Request failed. Please try again." + } + }; + + function normalizeLanguage(language) { + return language === "en" ? "en" : "zh"; + } + + function getLanguage() { + return normalizeLanguage(localStorage.getItem(STORAGE.language)); + } + + function setLanguage(language) { + const normalized = normalizeLanguage(language); + localStorage.setItem(STORAGE.language, normalized); + document.documentElement.lang = normalized === "zh" ? "zh-CN" : "en"; + window.dispatchEvent(new CustomEvent("learning-language-changed", { + detail: { language: normalized } + })); + } + + function t(key) { + const lang = getLanguage(); + return (TEXT[lang] && TEXT[lang][key]) || key; + } + + function getToken() { + return localStorage.getItem(STORAGE.token) || ""; + } + + function getUsername() { + return localStorage.getItem(STORAGE.username) || ""; + } + + function isLoggedIn() { + return Boolean(getToken()); + } + + function saveAuth(token, username) { + localStorage.setItem(STORAGE.token, token || ""); + localStorage.setItem(STORAGE.username, username || ""); + window.dispatchEvent(new CustomEvent("learning-auth-changed", { + detail: { loggedIn: isLoggedIn(), username: getUsername() } + })); + } + + function clearAuth() { + localStorage.removeItem(STORAGE.token); + localStorage.removeItem(STORAGE.username); + window.dispatchEvent(new CustomEvent("learning-auth-changed", { + detail: { loggedIn: false, username: "" } + })); + } + + function ensureStyle() { + if (document.getElementById("learning-shell-style")) { + return; + } + + const style = document.createElement("style"); + style.id = "learning-shell-style"; + style.textContent = [ + ".learning-shell {", + " max-width: 1380px;", + " margin: 18px auto 0;", + " padding: 0 24px;", + "}", + ".learning-shell-card {", + " display: flex;", + " justify-content: space-between;", + " align-items: center;", + " gap: 14px;", + " flex-wrap: wrap;", + " padding: 14px 18px;", + " border-radius: 22px;", + " border: 1px solid rgba(216, 228, 240, 0.9);", + " background: rgba(255, 255, 255, 0.88);", + " box-shadow: 0 12px 28px rgba(18, 32, 51, 0.08);", + " backdrop-filter: blur(10px);", + "}", + ".learning-shell-brand {", + " display: flex;", + " flex-direction: column;", + " gap: 4px;", + "}", + ".learning-shell-brand strong {", + " font-size: 15px;", + " color: #122033;", + "}", + ".learning-shell-brand span {", + " font-size: 13px;", + " color: #5d7288;", + "}", + ".learning-shell-actions {", + " display: flex;", + " align-items: center;", + " gap: 10px;", + " flex-wrap: wrap;", + "}", + ".learning-shell-link,", + ".learning-shell-button,", + ".learning-shell-badge {", + " display: inline-flex;", + " align-items: center;", + " justify-content: center;", + " min-height: 36px;", + " padding: 0 14px;", + " border-radius: 999px;", + " text-decoration: none;", + " font-size: 13px;", + " font-weight: 700;", + "}", + ".learning-shell-link {", + " color: #0f67b5;", + " background: rgba(15, 103, 181, 0.09);", + "}", + ".learning-shell-button {", + " border: 0;", + " cursor: pointer;", + " color: #fff;", + " background: linear-gradient(135deg, #177245, #35a465);", + "}", + ".learning-shell-button.alt {", + " color: #0f67b5;", + " background: rgba(15, 103, 181, 0.09);", + "}", + ".learning-shell-badge {", + " color: #33485e;", + " background: rgba(18, 32, 51, 0.06);", + "}", + "@media (max-width: 780px) {", + " .learning-shell {", + " padding: 0 14px;", + " }", + "}" + ].join("\n"); + document.head.appendChild(style); + } + + function mountShell(options) { + ensureStyle(); + + if (document.querySelector(".learning-shell")) { + if (options && typeof options.onLanguageChange === "function") { + options.onLanguageChange(getLanguage()); + } + return; + } + + const shell = document.createElement("div"); + shell.className = "learning-shell"; + shell.innerHTML = [ + '
', + '
', + ' ', + ' ', + "
", + '
', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + "
", + "
" + ].join(""); + + const firstPage = document.querySelector(".page"); + if (firstPage) { + document.body.insertBefore(shell, firstPage); + } else { + document.body.insertBefore(shell, document.body.firstChild); + } + + const els = { + brand: shell.querySelector('[data-role="brand"]'), + status: shell.querySelector('[data-role="status"]'), + home: shell.querySelector('[data-role="home"]'), + access: shell.querySelector('[data-role="access"]'), + user: shell.querySelector('[data-role="user"]'), + language: shell.querySelector('[data-role="language"]'), + login: shell.querySelector('[data-role="login"]'), + logout: shell.querySelector('[data-role="logout"]') + }; + + function render() { + const loggedIn = isLoggedIn(); + const username = getUsername(); + + els.brand.textContent = t("brand"); + els.status.textContent = loggedIn ? t("loginReady") : t("loginMissing"); + els.home.textContent = t("home"); + els.access.textContent = t("access"); + els.user.textContent = loggedIn ? t("currentUser") + ": " + username : t("loginMissing"); + els.language.textContent = t("languageToggle"); + els.login.textContent = t("login"); + els.logout.textContent = t("logout"); + els.login.style.display = loggedIn ? "none" : "inline-flex"; + els.logout.style.display = loggedIn ? "inline-flex" : "none"; + + if (options && typeof options.onLanguageChange === "function") { + options.onLanguageChange(getLanguage()); + } + if (options && typeof options.onAuthChange === "function") { + options.onAuthChange({ loggedIn: loggedIn, username: username }); + } + } + + els.language.addEventListener("click", function () { + setLanguage(getLanguage() === "zh" ? "en" : "zh"); + }); + + els.login.addEventListener("click", function () { + window.location.href = "/access.html"; + }); + + els.logout.addEventListener("click", function () { + clearAuth(); + render(); + }); + + window.addEventListener("learning-auth-changed", render); + window.addEventListener("learning-language-changed", render); + render(); + } + + async function fetchWithAuth(url, options) { + const requestOptions = options || {}; + const headers = new Headers(requestOptions.headers || {}); + const token = getToken(); + + if (token && !headers.has("Authorization")) { + headers.set("Authorization", "Bearer " + token); + } + + return fetch(url, Object.assign({}, requestOptions, { headers: headers })); + } + + async function requestJson(url, options) { + const response = await fetchWithAuth(url, options); + const contentType = response.headers.get("content-type") || ""; + const payload = contentType.includes("application/json") + ? await response.json() + : await response.text(); + + if (!response.ok) { + const error = new Error( + typeof payload === "object" && payload && payload.message + ? payload.message + : t("requestFailed") + ); + error.status = response.status; + error.payload = payload; + error.details = typeof payload === "object" && payload ? payload.data : null; + throw error; + } + + if ( + typeof payload === "object" && + payload && + Object.prototype.hasOwnProperty.call(payload, "code") && + payload.code !== 0 + ) { + const error = new Error(payload.message || t("requestFailed")); + error.status = payload.code; + error.payload = payload; + error.details = payload.data; + throw error; + } + + return payload; + } + + function describeError(error) { + if (error && Number(error.status) === 401) { + return t("unauthorized"); + } + return error && error.message ? error.message : t("requestFailed"); + } + + setLanguage(getLanguage()); + + window.learningShell = { + mountShell: mountShell, + getLanguage: getLanguage, + setLanguage: setLanguage, + getToken: getToken, + getUsername: getUsername, + isLoggedIn: isLoggedIn, + saveAuth: saveAuth, + clearAuth: clearAuth, + fetchWithAuth: fetchWithAuth, + requestJson: requestJson, + describeError: describeError + }; +})(); diff --git a/target/classes/static/users.html b/target/classes/static/users.html index 8a2a40c..7c4fbf4 100644 --- a/target/classes/static/users.html +++ b/target/classes/static/users.html @@ -3,224 +3,659 @@ - 用户管理 - Spring Boot + User Management Lab -

👥 用户管理 - RESTful API 示例

- -
-
-

用户列表

- -
- - - - - - - - - - - -
ID姓名邮箱年龄操作
-
- -
-

📖 学习要点

-
- RESTful API 设计: -
    -
  • GET /api/users - 获取所有用户
  • -
  • GET /api/users/{id} - 获取单个用户
  • -
  • POST /api/users - 创建用户
  • -
  • PUT /api/users/{id} - 更新用户
  • -
  • DELETE /api/users/{id} - 删除用户
  • -
-
- -

Controller 代码示例

-
@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) { ... }
-}
-
- -
-

🔧 Spring 注解说明

- - - - - - - - - -
注解说明
@RestController= @Controller + @ResponseBody
@RequestMapping定义路由映射
@GetMappingGET 请求映射
@PostMappingPOST 请求映射
@PathVariable获取路径变量
@RequestBody获取请求体 JSON
@RequestParam获取查询参数
-
- -

← 返回学习中心

- - -