feat: Spring Boot 学习脚手架 v2.0

- 新增 IoC 容器学习模块
- 新增 AOP 切面编程学习模块
- 新增 MyBatis 集成学习模块
- 新增事务管理学习模块
- 新增用户/产品/订单 CRUD
- 新增 7 个交互式学习页面
- 集成性能监控切面
This commit is contained in:
likingcode
2026-03-07 08:37:40 +00:00
commit c04235c655
73 changed files with 4978 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
package com.example.scaffold;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringbootScaffoldApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootScaffoldApplication.class, args);
}
}

View File

@@ -0,0 +1,115 @@
package com.example.scaffold.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* 学习切面 - 演示 AOP 各种通知类型
*
* 学习要点:
* 1. @Aspect - 标记为切面类
* 2. @Pointcut - 切入点表达式
* 3. @Before - 前置通知
* 4. @After - 后置通知
* 5. @AfterReturning - 返回通知
* 6. @AfterThrowing - 异常通知
* 7. @Around - 环绕通知
*/
@Slf4j
@Aspect
@Component
public class LearningAspect {
/**
* 切入点 - 所有 Service 层方法
*/
@Pointcut("execution(* com.example.scaffold.service..*.*(..))")
public void serviceLayer() {}
/**
* 切入点 - 所有 Controller 层方法
*/
@Pointcut("execution(* com.example.scaffold.controller..*.*(..))")
public void controllerLayer() {}
/**
* 切入点 - 所有 Mapper 方法
*/
@Pointcut("execution(* com.example.scaffold.mapper..*.*(..))")
public void mapperLayer() {}
/**
* 前置通知 - 方法执行前
*/
@Before("serviceLayer()")
public void beforeService(JoinPoint joinPoint) {
log.info("🔹 [AOP @Before] 即将执行: {}.{}()",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName());
}
/**
* 后置通知 - 方法执行后(无论是否异常)
*/
@After("serviceLayer()")
public void afterService(JoinPoint joinPoint) {
log.info("🔸 [AOP @After] 执行完成: {}.{}()",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName());
}
/**
* 返回通知 - 方法成功返回后
*/
@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void afterReturningService(JoinPoint joinPoint, Object result) {
log.info("✅ [AOP @AfterReturning] 方法返回: {}.{}() => {}",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName(),
result != null ? result.getClass().getSimpleName() : "null");
}
/**
* 异常通知 - 方法抛出异常后
*/
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void afterThrowingService(JoinPoint joinPoint, Throwable ex) {
log.error("❌ [AOP @AfterThrowing] 方法异常: {}.{}() => {}",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName(),
ex.getMessage());
}
/**
* 环绕通知 - 完全控制方法执行
*/
@Around("controllerLayer()")
public Object aroundController(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info("🔄 [AOP @Around] 开始: {}.{}() 参数: {}", className, methodName, Arrays.toString(args));
try {
// 执行目标方法
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - startTime;
log.info("🔄 [AOP @Around] 完成: {}.{}() 耗时: {}ms", className, methodName, duration);
return result;
} catch (Throwable ex) {
long duration = System.currentTimeMillis() - startTime;
log.error("🔄 [AOP @Around] 异常: {}.{}() 耗时: {}ms 错误: {}",
className, methodName, duration, ex.getMessage());
throw ex;
}
}
}

View File

@@ -0,0 +1,67 @@
package com.example.scaffold.aop;
import lombok.extern.slf4j.Slf4j;
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.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* 性能监控切面 - 统计方法执行时间
*/
@Slf4j
@Aspect
@Component
public class PerformanceAspect {
private final Map<String, MethodStats> statsMap = new ConcurrentHashMap<>();
public static class MethodStats {
public final AtomicLong totalCount = new AtomicLong();
public final AtomicLong totalTime = new AtomicLong();
public final AtomicLong maxTime = new AtomicLong();
public final AtomicLong errorCount = new AtomicLong();
}
@Around("execution(* com.example.scaffold..*.*(..))")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
String key = joinPoint.getTarget().getClass().getSimpleName() + "." + joinPoint.getSignature().getName();
long startTime = System.nanoTime();
try {
Object result = joinPoint.proceed();
recordSuccess(key, System.nanoTime() - startTime);
return result;
} catch (Throwable ex) {
recordError(key, System.nanoTime() - startTime);
throw ex;
}
}
private void recordSuccess(String key, long durationNanos) {
MethodStats stats = statsMap.computeIfAbsent(key, k -> new MethodStats());
stats.totalCount.incrementAndGet();
stats.totalTime.addAndGet(durationNanos);
long durationMs = durationNanos / 1_000_000;
stats.maxTime.updateAndGet(current -> Math.max(current, durationMs));
}
private void recordError(String key, long durationNanos) {
MethodStats stats = statsMap.computeIfAbsent(key, k -> new MethodStats());
stats.totalCount.incrementAndGet();
stats.errorCount.incrementAndGet();
stats.totalTime.addAndGet(durationNanos);
}
public Map<String, MethodStats> getStats() {
return new ConcurrentHashMap<>(statsMap);
}
public void resetStats() {
statsMap.clear();
}
}

View File

@@ -0,0 +1,38 @@
package com.example.scaffold.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* 应用配置类
*
* 学习要点:
* 1. @Configuration - 标记为配置类
* 2. @Bean - 声明 Bean
* 3. @EnableJpaAuditing - 启用 JPA 审计
*/
@Configuration
@EnableJpaAuditing
public class AppConfig {
/**
* CORS 配置 - 允许跨域请求
*/
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}

View File

@@ -0,0 +1,18 @@
package com.example.scaffold.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/api/hello")
public String hello() {
return "Hello from Spring Boot Scaffold!";
}
@GetMapping("/api/health")
public String health() {
return "OK";
}
}

View File

@@ -0,0 +1,125 @@
package com.example.scaffold.controller;
import com.example.scaffold.dto.OrderCreateRequest;
import com.example.scaffold.dto.ProductCreateRequest;
import com.example.scaffold.entity.Order;
import com.example.scaffold.entity.Product;
import com.example.scaffold.mapper.ProductMapper;
import com.example.scaffold.service.impl.OrderService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 产品和订单控制器 - 演示事务
*/
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class ProductOrderController {
private final ProductMapper productMapper;
private final OrderService orderService;
// ==================== 产品 API ====================
@GetMapping("/products")
public List<Product> listProducts() {
return productMapper.findAll();
}
@GetMapping("/products/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
Product product = productMapper.findById(id);
return product != null ? ResponseEntity.ok(product) : ResponseEntity.notFound().build();
}
@GetMapping("/products/category/{category}")
public List<Product> getByCategory(@PathVariable String category) {
return productMapper.findByCategory(category);
}
@GetMapping("/products/price")
public List<Product> getByPriceRange(@RequestParam BigDecimal min, @RequestParam BigDecimal max) {
return productMapper.findByPriceRange(min, max);
}
@PostMapping("/products")
public ResponseEntity<Product> createProduct(@Valid @RequestBody ProductCreateRequest request) {
Product product = new Product();
product.setName(request.getName());
product.setDescription(request.getDescription());
product.setPrice(request.getPrice());
product.setStockQuantity(request.getStockQuantity());
product.setCategory(request.getCategory());
productMapper.insert(product);
return ResponseEntity.ok(product);
}
@DeleteMapping("/products/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productMapper.deleteById(id);
return ResponseEntity.noContent().build();
}
// ==================== 订单 API ====================
@GetMapping("/orders")
public List<Order> listOrders() {
return orderService.findAll();
}
@GetMapping("/orders/{id}")
public ResponseEntity<Order> getOrder(@PathVariable Long id) {
Order order = orderService.getOrder(id);
return order != null ? ResponseEntity.ok(order) : ResponseEntity.notFound().build();
}
@GetMapping("/orders/user/{userId}")
public List<Order> getOrdersByUser(@PathVariable Long userId) {
return orderService.findByUserId(userId);
}
/**
* 创建订单 - 演示事务
*/
@PostMapping("/orders")
public ResponseEntity<?> createOrder(@Valid @RequestBody OrderCreateRequest request) {
try {
Order order = orderService.createOrderWithRollback(
request.getUserId(),
request.getProductId(),
request.getQuantity(),
request.isRollback()
);
return ResponseEntity.ok(Map.of(
"success", true,
"order", order,
"message", request.isRollback() ? "事务已回滚但订单仍创建REQUIRES_NEW演示" : "订单创建成功"
));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"error", e.getMessage()
));
}
}
@PatchMapping("/orders/{id}/status")
public ResponseEntity<Void> updateOrderStatus(@PathVariable Long id, @RequestParam String status) {
orderService.updateStatus(id, status);
return ResponseEntity.ok().build();
}
@DeleteMapping("/orders/{id}")
public ResponseEntity<Void> deleteOrder(@PathVariable Long id) {
orderService.deleteById(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -0,0 +1,13 @@
package com.example.scaffold.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RootController {
@GetMapping("/")
public String root() {
return "Spring Boot Scaffold is running!";
}
}

View File

@@ -0,0 +1,100 @@
package com.example.scaffold.controller;
import com.example.scaffold.dto.UserCreateRequest;
import com.example.scaffold.entity.User;
import com.example.scaffold.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 用户控制器 - RESTful API 演示
*/
@Slf4j
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/**
* GET - 查询所有用户
*/
@GetMapping
public List<User> list() {
return userService.findAll();
}
/**
* GET - 根据 ID 查询
*/
@GetMapping("/{id}")
public ResponseEntity<User> getById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
/**
* GET - 搜索用户
*/
@GetMapping("/search")
public List<User> search(@RequestParam String username) {
return userService.searchByUsername(username);
}
/**
* POST - 创建用户
*/
@PostMapping
public ResponseEntity<User> create(@Valid @RequestBody UserCreateRequest request) {
User user = new User();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setPhone(request.getPhone());
user.setBio(request.getBio());
user.setActive(true);
User saved = userService.save(user);
return ResponseEntity.ok(saved);
}
/**
* PUT - 更新用户
*/
@PutMapping("/{id}")
public ResponseEntity<User> update(@PathVariable Long id, @Valid @RequestBody UserCreateRequest request) {
return userService.findById(id)
.map(existing -> {
existing.setUsername(request.getUsername());
existing.setEmail(request.getEmail());
existing.setPhone(request.getPhone());
existing.setBio(request.getBio());
return ResponseEntity.ok(userService.save(existing));
})
.orElse(ResponseEntity.notFound().build());
}
/**
* DELETE - 删除用户
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
userService.deleteById(id);
return ResponseEntity.noContent().build();
}
/**
* GET - 统计
*/
@GetMapping("/count")
public Map<String, Long> count() {
return Map.of("count", userService.count());
}
}

View File

@@ -0,0 +1,25 @@
package com.example.scaffold.dto;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 订单创建请求 DTO
*/
@Data
public class OrderCreateRequest {
@NotNull(message = "用户ID不能为空")
private Long userId;
@NotNull(message = "产品ID不能为空")
private Long productId;
@NotNull(message = "数量不能为空")
@Min(value = 1, message = "数量至少为1")
private Integer quantity;
/** 是否触发回滚(用于演示事务) */
private boolean rollback = false;
}

View File

@@ -0,0 +1,28 @@
package com.example.scaffold.dto;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
/**
* 产品创建请求 DTO
*/
@Data
public class ProductCreateRequest {
@NotNull(message = "产品名称不能为空")
private String name;
private String description;
@NotNull(message = "价格不能为空")
@Min(value = 0, message = "价格不能为负数")
private BigDecimal price;
@Min(value = 0, message = "库存不能为负数")
private Integer stockQuantity = 0;
private String category;
}

View File

@@ -0,0 +1,33 @@
package com.example.scaffold.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 用户创建请求 DTO
*
* 学习要点:
* 1. @Data - Lombok 自动生成 getter/setter
* 2. @NotBlank - 验证非空
* 3. @Size - 验证长度
* 4. @Email - 验证邮箱格式
*/
@Data
public class UserCreateRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 50, message = "用户名长度必须在2-50之间")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@Size(max = 20, message = "手机号最长20位")
private String phone;
@Size(max = 500, message = "简介最长500字")
private String bio;
}

View File

@@ -0,0 +1,42 @@
package com.example.scaffold.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单实体 - 用于演示事务传播
*/
@Data
@Entity
@Table(name = "orders")
@EntityListeners(AuditingEntityListener.class)
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "product_id", nullable = false)
private Long productId;
@Column(nullable = false)
private Integer quantity = 1;
@Column(name = "total_price", nullable = false, precision = 10, scale = 2)
private BigDecimal totalPrice;
@Column(length = 20)
private String status = "PENDING"; // PENDING, PAID, SHIPPED, COMPLETED, CANCELLED
@Column(name = "created_at", updatable = false)
@CreatedDate
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,42 @@
package com.example.scaffold.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 产品实体 - 用于演示事务和复杂查询
*/
@Data
@Entity
@Table(name = "products")
@EntityListeners(AuditingEntityListener.class)
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(length = 500)
private String description;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@Column(name = "stock_quantity")
private Integer stockQuantity = 0;
@Column(length = 50)
private String category;
@Column(name = "created_at", updatable = false)
@CreatedDate
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,101 @@
package com.example.scaffold.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
/**
* 用户实体 - 用于演示 JPA 和 MyBatis
*
* 学习要点:
* 1. @Entity - 标记为 JPA 实体
* 2. @Table - 指定表名
* 3. @Id + @GeneratedValue - 主键策略
* 4. @Column - 列映射
* 5. @Data - Lombok 自动生成 getter/setter
*/
@Data
@Entity
@Table(name = "users")
@EntityListeners(AuditingEntityListener.class)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String username;
@Column(nullable = false, length = 100)
private String email;
@Column(length = 20)
private String phone;
@Column(columnDefinition = "TEXT")
private String bio;
@Column(nullable = false)
private Boolean active = true;
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
/**
* 生命周期回调 - 持久化前
*/
@PrePersist
public void prePersist() {
System.out.println("🔔 [User @PrePersist] 即将保存用户: " + username);
}
/**
* 生命周期回调 - 持久化后
*/
@PostPersist
public void postPersist() {
System.out.println("✅ [User @PostPersist] 用户已保存, ID: " + id);
}
/**
* 生命周期回调 - 更新前
*/
@PreUpdate
public void preUpdate() {
System.out.println("🔔 [User @PreUpdate] 即将更新用户: " + id);
}
/**
* 生命周期回调 - 更新后
*/
@PostUpdate
public void postUpdate() {
System.out.println("✅ [User @PostUpdate] 用户已更新: " + id);
}
/**
* 生命周期回调 - 删除前
*/
@PreRemove
public void preRemove() {
System.out.println("🔔 [User @PreRemove] 即将删除用户: " + id);
}
/**
* 生命周期回调 - 删除后
*/
@PostRemove
public void postRemove() {
System.out.println("✅ [User @PostRemove] 用户已删除: " + id);
}
}

View File

@@ -0,0 +1,110 @@
package com.example.scaffold.learning;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* AOP 学习控制器
*
* 学习要点:
* 1. 切面概念
* 2. 通知类型
* 3. 切入点表达式
*/
@Slf4j
@RestController
@RequestMapping("/api/learning/aop")
@RequiredArgsConstructor
public class AopLearningController {
/**
* AOP 概念说明
*/
@GetMapping("/concepts")
public Map<String, Object> concepts() {
return Map.of(
"AOP", "面向切面编程 (Aspect Oriented Programming)",
"核心概念", Map.of(
"Aspect (切面)", "横切关注点的模块化",
"JoinPoint (连接点)", "程序执行的某个特定位置",
"Pointcut (切入点)", "匹配连接点的表达式",
"Advice (通知)", "在切入点执行的代码",
"Target (目标对象)", "被通知的对象",
"Proxy (代理)", "AOP 创建的代理对象",
"Weaving (织入)", "将切面应用到目标对象的过程"
),
"通知类型", Map.of(
"@Before", "方法执行前",
"@After", "方法执行后(无论是否异常)",
"@AfterReturning", "方法成功返回后",
"@AfterThrowing", "方法抛出异常后",
"@Around", "环绕 - 完全控制方法执行"
)
);
}
/**
* 切入点表达式语法
*/
@GetMapping("/pointcut-syntax")
public Map<String, Object> pointcutSyntax() {
return Map.of(
"语法", "execution(修饰符? 返回类型 包名.类名.方法名(参数) 异常?)",
"示例", List.of(
"execution(* com.example.service.*.*(..)) - service包下所有方法",
"execution(* com.example.service..*.*(..)) - service包及子包所有方法",
"execution(public * *(..)) - 所有public方法",
"execution(* set*(..)) - 所有set开头的方法",
"execution(* com.example.service.UserService.*(..)) - UserService的所有方法",
"@annotation(org.springframework.transaction.annotation.Transactional) - 带@Transactional的方法"
),
"通配符", Map.of(
"*", "匹配任意字符",
"..", "匹配任意层级的包或任意参数",
"+", "匹配指定类及其子类"
)
);
}
/**
* 测试 AOP - 这个方法会被切面拦截
*/
@GetMapping("/test")
public Map<String, Object> testAop(@RequestParam(defaultValue = "test") String message) {
log.info("📝 [AopLearningController] 测试方法被调用: message={}", message);
return Map.of(
"message", "AOP 测试成功",
"input", message,
"tip", "查看控制台日志,观察 AOP 通知的执行顺序",
"expectedOrder", List.of(
"1. @Around 开始",
"2. @Before",
"3. 方法执行",
"4. @AfterReturning",
"5. @After",
"6. @Around 结束"
)
);
}
/**
* 演示异常通知
*/
@GetMapping("/test-error")
public Map<String, Object> testError(@RequestParam(defaultValue = "false") boolean error) {
log.info("📝 [AopLearningController] 测试异常通知: error={}", error);
if (error) {
throw new RuntimeException("这是一个测试异常,用于触发 @AfterThrowing");
}
return Map.of(
"message", "正常执行",
"tip", "传入 error=true 触发异常,观察 @AfterThrowing"
);
}
}

View File

@@ -0,0 +1,164 @@
package com.example.scaffold.learning;
import com.example.scaffold.aop.PerformanceAspect;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Scope;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.WebApplicationContext;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.util.*;
/**
* IoC 容器学习控制器
*
* 学习要点:
* 1. Bean 的生命周期
* 2. 依赖注入方式
* 3. Bean 作用域
* 4. 条件化配置
*/
@RestController
@RequestMapping("/api/learning/ioc")
@RequiredArgsConstructor
public class IocLearningController {
private final ApplicationContext applicationContext;
private final PerformanceAspect performanceAspect;
// 演示字段注入(不推荐,但可以用)
@Autowired
@Qualifier("learningBean")
private LearningBean learningBean;
/**
* 查看所有 Bean
*/
@GetMapping("/beans")
public Map<String, Object> listBeans() {
String[] beanNames = applicationContext.getBeanDefinitionNames();
Map<String, Object> result = new LinkedHashMap<>();
result.put("total", beanNames.length);
result.put("userBeans", Arrays.stream(beanNames)
.filter(name -> name.startsWith("user") || name.startsWith("learning") ||
name.contains("Service") || name.contains("Controller") || name.contains("Mapper"))
.sorted()
.toList());
result.put("allBeans", Arrays.stream(beanNames).sorted().toList());
return result;
}
/**
* 查看 Bean 详情
*/
@GetMapping("/beans/{name}")
public Map<String, Object> getBeanDetail(@PathVariable String name) {
try {
Object bean = applicationContext.getBean(name);
Class<?> clazz = bean.getClass();
return Map.of(
"name", name,
"type", clazz.getName(),
"simpleName", clazz.getSimpleName(),
"interfaces", Arrays.toString(clazz.getInterfaces()),
"annotations", Arrays.toString(clazz.getAnnotations()),
"scope", applicationContext.isSingleton(name) ? "singleton" : "prototype"
);
} catch (BeansException e) {
return Map.of("error", "Bean not found: " + name);
}
}
/**
* 演示依赖注入方式
*/
@GetMapping("/injection-types")
public Map<String, String> injectionTypes() {
return Map.of(
"构造器注入", "推荐!明确依赖,不可变,易于测试",
"Setter注入", "可选依赖,灵活性高",
"字段注入", "不推荐!隐藏依赖,难以测试",
"本控制器使用", "构造器注入 (RequiredArgsConstructor) + 字段注入演示"
);
}
/**
* 演示 Bean 作用域
*/
@GetMapping("/scopes")
public Map<String, Object> scopes() {
return Map.of(
"singleton", "单例 - 整个应用只有一个实例(默认)",
"prototype", "原型 - 每次请求都创建新实例",
"request", "请求 - 每个 HTTP 请求一个实例",
"session", "会话 - 每个 HTTP 会话一个实例",
"demo", learningBean.getInstanceInfo()
);
}
/**
* 性能统计
*/
@GetMapping("/performance")
public Map<String, Object> getPerformance() {
var stats = performanceAspect.getStats();
Map<String, Object> result = new LinkedHashMap<>();
stats.forEach((key, value) -> {
long totalMs = value.totalTime.get() / 1_000_000;
long avgMs = value.totalCount.get() > 0 ? totalMs / value.totalCount.get() : 0;
result.put(key, Map.of(
"count", value.totalCount.get(),
"errors", value.errorCount.get(),
"totalMs", totalMs,
"avgMs", avgMs,
"maxMs", value.maxTime.get()
));
});
return result;
}
/**
* 重置性能统计
*/
@PostMapping("/performance/reset")
public Map<String, String> resetPerformance() {
performanceAspect.resetStats();
return Map.of("status", "ok", "message", "性能统计已重置");
}
/**
* 学习 Bean - 演示作用域和生命周期
*/
@org.springframework.stereotype.Component("learningBean")
@Scope(WebApplicationContext.SCOPE_SESSION)
public static class LearningBean {
private final String instanceId = UUID.randomUUID().toString().substring(0, 8);
private int accessCount = 0;
@PostConstruct
public void init() {
System.out.println("🟢 [LearningBean @PostConstruct] Bean 初始化: " + instanceId);
}
@PreDestroy
public void destroy() {
System.out.println("🔴 [LearningBean @PreDestroy] Bean 销毁: " + instanceId);
}
public String getInstanceInfo() {
accessCount++;
return String.format("实例ID: %s, 访问次数: %d, 作用域: session", instanceId, accessCount);
}
}
}

View File

@@ -0,0 +1,127 @@
package com.example.scaffold.learning;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* MyBatis 学习控制器
*
* 学习要点:
* 1. MyBatis vs JPA 对比
* 2. 动态 SQL
* 3. 缓存机制
* 4. 结果映射
*/
@Slf4j
@RestController
@RequestMapping("/api/learning/mybatis")
@RequiredArgsConstructor
public class MyBatisLearningController {
private final SqlSessionFactory sqlSessionFactory;
/**
* MyBatis 核心概念
*/
@GetMapping("/concepts")
public Map<String, Object> concepts() {
return Map.of(
"MyBatis", "半自动 ORM 框架SQL 与 Java 对象映射",
"核心组件", Map.of(
"SqlSessionFactory", "创建 SqlSession 的工厂",
"SqlSession", "执行 SQL 的会话",
"Mapper", "接口绑定的 SQL 语句",
"Configuration", "MyBatis 配置信息"
),
"缓存", Map.of(
"一级缓存", "SqlSession 级别,默认开启",
"二级缓存", "Mapper 级别,需配置 @CacheNamespace"
)
);
}
/**
* MyBatis vs JPA 对比
*/
@GetMapping("/vs-jpa")
public Map<String, Object> vsJpa() {
return Map.of(
"MyBatis", Map.of(
"优点", List.of("SQL 灵活可控", "性能优化方便", "复杂查询友好"),
"缺点", List.of("SQL 与代码耦合", "数据库迁移成本高", "需要手写 SQL"),
"适用场景", "复杂查询、性能要求高、DBA 参与项目"
),
"JPA/Hibernate", Map.of(
"优点", List.of("面向对象", "数据库无关", "开发效率高"),
"缺点", List.of("复杂查询困难", "性能调优复杂", "学习曲线陡"),
"适用场景", "标准 CRUD、快速开发、领域驱动设计"
),
"建议", "小型项目用 JPA大型/复杂查询项目用 MyBatis"
);
}
/**
* 动态 SQL 语法
*/
@GetMapping("/dynamic-sql")
public Map<String, Object> dynamicSql() {
return Map.of(
"if", "条件判断 <if test='name != null'> AND name = #{name} </if>",
"choose/when/otherwise", "类似 switch-case",
"trim/where/set", "处理 SQL 拼接",
"foreach", "循环遍历 <foreach item='item' collection='list' separator=','>#{item}</foreach>",
"bind", "定义变量 <bind name='pattern' value=\"'%' + name + '%'\" />",
"示例", """
<select id='findUser'>
SELECT * FROM users
<where>
<if test='name != null'>AND name LIKE #{name}</if>
<if test='email != null'>AND email = #{email}</if>
</where>
</select>
"""
);
}
/**
* 缓存演示
*/
@GetMapping("/cache")
public Map<String, Object> cacheDemo() {
return Map.of(
"一级缓存", Map.of(
"范围", "SqlSession 级别",
"默认", "开启",
"失效条件", List.of("执行 insert/update/delete", "调用 sqlSession.clearCache()", "事务提交/回滚"),
"演示", "同一 SqlSession 内连续两次相同查询,第二次不执行 SQL"
),
"二级缓存", Map.of(
"范围", "Mapper 级别",
"配置", "@CacheNamespace 或 XML 配置",
"注意", "实体类需要实现 Serializable",
"本项目", "UserMapper 已启用二级缓存"
),
"验证方式", "查看控制台 SQL 日志,缓存命中时不打印 SQL"
);
}
/**
* 查看当前配置
*/
@GetMapping("/config")
public Map<String, Object> getConfig() {
var config = sqlSessionFactory.getConfiguration();
return Map.of(
"cacheEnabled", config.isCacheEnabled(),
"localCacheScope", config.getLocalCacheScope().name(),
"defaultExecutorType", config.getDefaultExecutorType().name(),
"mapUnderscoreToCamelCase", config.isMapUnderscoreToCamelCase(),
"mappedStatements", config.getMappedStatements().size()
);
}
}

View File

@@ -0,0 +1,135 @@
package com.example.scaffold.learning;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* 事务学习控制器
*
* 学习要点:
* 1. 事务传播行为
* 2. 事务隔离级别
* 3. 事务回滚
*/
@Slf4j
@RestController
@RequestMapping("/api/learning/transaction")
@RequiredArgsConstructor
public class TransactionLearningController {
/**
* 事务概念
*/
@GetMapping("/concepts")
public Map<String, Object> concepts() {
return Map.of(
"ACID", Map.of(
"Atomicity (原子性)", "事务是不可分割的工作单位",
"Consistency (一致性)", "事务必须使数据库从一个一致性状态变换到另一个一致性状态",
"Isolation (隔离性)", "多个用户并发访问数据库时,数据库为每个用户开启的事务不能被其他事务干扰",
"Durability (持久性)", "事务一旦提交,对数据库的改变是永久性的"
),
"Spring事务", "@Transactional 注解实现声明式事务管理"
);
}
/**
* 传播行为详解
*/
@GetMapping("/propagation")
public Map<String, Object> propagation() {
return Map.of(
"REQUIRED (默认)", Map.of(
"描述", "有事务则加入,无则新建",
"场景", "最常用,大多数业务方法",
"示例", "A 调用 BB 加入 A 的事务"
),
"REQUIRES_NEW", Map.of(
"描述", "总是新建事务,挂起当前事务",
"场景", "日志记录、独立子任务",
"示例", "A 调用 BB 在新事务执行A 回滚不影响 B"
),
"SUPPORTS", Map.of(
"描述", "有事务则加入,无则以非事务运行",
"场景", "查询方法"
),
"NOT_SUPPORTED", Map.of(
"描述", "以非事务运行,挂起当前事务",
"场景", "不需要事务的操作"
),
"MANDATORY", Map.of(
"描述", "必须在事务中运行,否则抛异常",
"场景", "强制要求事务"
),
"NEVER", Map.of(
"描述", "不能在事务中运行,否则抛异常",
"场景", "确保无事务"
),
"NESTED", Map.of(
"描述", "嵌套事务,可独立回滚",
"场景", "部分失败不影响整体"
)
);
}
/**
* 隔离级别详解
*/
@GetMapping("/isolation")
public Map<String, Object> isolation() {
return Map.of(
"DEFAULT", "使用数据库默认隔离级别",
"READ_UNCOMMITTED", Map.of(
"描述", "读未提交",
"问题", "脏读、不可重复读、幻读",
"性能", "最高"
),
"READ_COMMITTED", Map.of(
"描述", "读已提交",
"问题", "不可重复读、幻读",
"性能", "较高",
"场景", "大多数数据库默认"
),
"REPEATABLE_READ", Map.of(
"描述", "可重复读",
"问题", "幻读",
"性能", "中等",
"场景", "MySQL 默认"
),
"SERIALIZABLE", Map.of(
"描述", "串行化",
"问题", "",
"性能", "最低",
"场景", "数据一致性要求极高"
),
"问题说明", Map.of(
"脏读", "读到其他事务未提交的数据",
"不可重复读", "同一事务两次读取结果不同(修改导致)",
"幻读", "同一事务两次读取结果不同(新增/删除导致)"
)
);
}
/**
* 回滚规则
*/
@GetMapping("/rollback")
public Map<String, Object> rollback() {
return Map.of(
"默认行为", "只对 RuntimeException 和 Error 回滚",
"rollbackFor", "指定需要回滚的异常类型",
"noRollbackFor", "指定不需要回滚的异常类型",
"示例", """
@Transactional(rollbackFor = Exception.class)
@Transactional(noRollbackFor = BusinessException.class)
""",
"测试API", Map.of(
"创建订单", "POST /api/orders",
"创建订单(回滚)", "POST /api/orders?rollback=true"
)
);
}
}

View File

@@ -0,0 +1,33 @@
package com.example.scaffold.mapper;
import com.example.scaffold.entity.Order;
import org.apache.ibatis.annotations.*;
import java.util.List;
/**
* Order MyBatis Mapper
*/
@Mapper
public interface OrderMapper {
@Select("SELECT * FROM orders ORDER BY created_at DESC")
List<Order> findAll();
@Select("SELECT * FROM orders WHERE id = #{id}")
Order findById(@Param("id") Long id);
@Select("SELECT * FROM orders WHERE user_id = #{userId}")
List<Order> findByUserId(@Param("userId") Long userId);
@Insert("INSERT INTO orders(user_id, product_id, quantity, total_price, status, created_at) " +
"VALUES(#{userId}, #{productId}, #{quantity}, #{totalPrice}, #{status}, NOW())")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(Order order);
@Update("UPDATE orders SET status = #{status} WHERE id = #{id}")
int updateStatus(@Param("id") Long id, @Param("status") String status);
@Delete("DELETE FROM orders WHERE id = #{id}")
int deleteById(@Param("id") Long id);
}

View File

@@ -0,0 +1,40 @@
package com.example.scaffold.mapper;
import com.example.scaffold.entity.Product;
import org.apache.ibatis.annotations.*;
import java.math.BigDecimal;
import java.util.List;
/**
* Product MyBatis Mapper - 演示复杂查询
*/
@Mapper
public interface ProductMapper {
@Select("SELECT * FROM products ORDER BY created_at DESC")
List<Product> findAll();
@Select("SELECT * FROM products WHERE id = #{id}")
Product findById(@Param("id") Long id);
@Select("SELECT * FROM products WHERE category = #{category}")
List<Product> findByCategory(@Param("category") String category);
@Select("SELECT * FROM products WHERE price BETWEEN #{min} AND #{max} ORDER BY price")
List<Product> findByPriceRange(@Param("min") BigDecimal min, @Param("max") BigDecimal max);
@Insert("INSERT INTO products(name, description, price, stock_quantity, category, created_at) " +
"VALUES(#{name}, #{description}, #{price}, #{stockQuantity}, #{category}, NOW())")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(Product product);
@Update("UPDATE products SET stock_quantity = stock_quantity - #{quantity} WHERE id = #{id} AND stock_quantity >= #{quantity}")
int decreaseStock(@Param("id") Long id, @Param("quantity") Integer quantity);
@Update("UPDATE products SET stock_quantity = stock_quantity + #{quantity} WHERE id = #{id}")
int increaseStock(@Param("id") Long id, @Param("quantity") Integer quantity);
@Delete("DELETE FROM products WHERE id = #{id}")
int deleteById(@Param("id") Long id);
}

View File

@@ -0,0 +1,78 @@
package com.example.scaffold.mapper;
import com.example.scaffold.entity.User;
import org.apache.ibatis.annotations.*;
import org.apache.ibatis.cache.decorators.LruCache;
import java.util.List;
/**
* User MyBatis Mapper - 演示 MyBatis 注解方式
*
* 学习要点:
* 1. @Mapper - 标记为 MyBatis Mapper 接口
* 2. @Select/@Insert/@Update/@Delete - SQL 注解
* 3. @Options - 额外选项如返回自增ID
* 4. @ResultMap - 结果映射
* 5. @CacheNamespace - 二级缓存
*/
@Mapper
@CacheNamespace(implementation = LruCache.class, size = 1024)
public interface UserMapper {
/**
* 查询所有用户
*/
@Select("SELECT * FROM users ORDER BY created_at DESC")
List<User> findAll();
/**
* 根据ID查询 - 使用一级缓存
*/
@Select("SELECT * FROM users WHERE id = #{id}")
User findById(@Param("id") Long id);
/**
* 根据用户名模糊查询 - 动态SQL演示
*/
@Select("SELECT * FROM users WHERE username LIKE CONCAT('%', #{username}, '%')")
List<User> findByUsernameLike(@Param("username") String username);
/**
* 插入用户 - 返回自增ID
*/
@Insert("INSERT INTO users(username, email, phone, bio, active, created_at, updated_at) " +
"VALUES(#{username}, #{email}, #{phone}, #{bio}, #{active}, NOW(), NOW())")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
/**
* 更新用户
*/
@Update("UPDATE users SET username=#{username}, email=#{email}, phone=#{phone}, " +
"bio=#{bio}, active=#{active}, updated_at=NOW() WHERE id=#{id}")
int update(User user);
/**
* 删除用户
*/
@Delete("DELETE FROM users WHERE id = #{id}")
int deleteById(@Param("id") Long id);
/**
* 统计用户数量
*/
@Select("SELECT COUNT(*) FROM users")
long count();
/**
* 批量插入 - 演示脚本SQL
*/
@Insert("<script>" +
"INSERT INTO users(username, email, phone, active, created_at, updated_at) VALUES " +
"<foreach collection='users' item='u' separator=','>" +
"(#{u.username}, #{u.email}, #{u.phone}, #{u.active}, NOW(), NOW())" +
"</foreach>" +
"</script>")
int batchInsert(@Param("users") List<User> users);
}

View File

@@ -0,0 +1,24 @@
package com.example.scaffold.service;
import com.example.scaffold.entity.User;
import java.util.List;
import java.util.Optional;
/**
* 用户服务接口 - 演示接口与实现分离
*/
public interface UserService {
List<User> findAll();
Optional<User> findById(Long id);
User save(User user);
void deleteById(Long id);
List<User> searchByUsername(String username);
long count();
}

View File

@@ -0,0 +1,126 @@
package com.example.scaffold.service.impl;
import com.example.scaffold.entity.Order;
import com.example.scaffold.entity.Product;
import com.example.scaffold.mapper.OrderMapper;
import com.example.scaffold.mapper.ProductMapper;
import com.example.scaffold.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
/**
* 订单服务 - 演示事务传播和隔离级别
*
* 学习要点:
* 1. 事务传播行为 - Propagation
* 2. 事务隔离级别 - Isolation
* 3. 事务回滚 - rollbackFor
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderMapper orderMapper;
private final ProductMapper productMapper;
private final UserMapper userMapper;
/**
* 创建订单 - 演示事务
* REQUIRED: 有事务则加入,无则新建
*/
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public Order createOrder(Long userId, Long productId, Integer quantity) {
log.info("📦 [OrderService] 创建订单: userId={}, productId={}, quantity={}", userId, productId, quantity);
// 检查用户
if (userMapper.findById(userId) == null) {
throw new RuntimeException("用户不存在: " + userId);
}
// 检查产品
Product product = productMapper.findById(productId);
if (product == null) {
throw new RuntimeException("产品不存在: " + productId);
}
// 扣减库存
int rows = productMapper.decreaseStock(productId, quantity);
if (rows == 0) {
throw new RuntimeException("库存不足");
}
// 创建订单
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setQuantity(quantity);
order.setTotalPrice(product.getPrice().multiply(BigDecimal.valueOf(quantity)));
order.setStatus("PENDING");
orderMapper.insert(order);
log.info("✅ [OrderService] 订单创建成功: orderId={}", order.getId());
return order;
}
/**
* 模拟事务回滚
*/
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public Order createOrderWithRollback(Long userId, Long productId, Integer quantity, boolean shouldRollback) {
log.info("📦 [OrderService] 创建订单(可能回滚): userId={}, shouldRollback={}", userId, shouldRollback);
Order order = createOrder(userId, productId, quantity);
if (shouldRollback) {
log.warn("⚠️ [OrderService] 触发回滚!");
throw new RuntimeException("模拟事务回滚");
}
return order;
}
/**
* REQUIRES_NEW - 挂起当前事务,创建新事务
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOrderOperation(Long orderId, String operation) {
log.info("📝 [OrderService] 记录订单操作: orderId={}, operation={}", orderId, operation);
// 这个方法会在独立事务中执行
// 即使外部事务回滚,这里的记录也会保留
}
/**
* 使用隔离级别 READ_COMMITTED
*/
@Transactional(isolation = Isolation.READ_COMMITTED)
public Order getOrder(Long id) {
return orderMapper.findById(id);
}
public List<Order> findAll() {
return orderMapper.findAll();
}
public List<Order> findByUserId(Long userId) {
return orderMapper.findByUserId(userId);
}
@Transactional
public void updateStatus(Long id, String status) {
orderMapper.updateStatus(id, status);
}
@Transactional
public void deleteById(Long id) {
orderMapper.deleteById(id);
}
}

View File

@@ -0,0 +1,77 @@
package com.example.scaffold.service.impl;
import com.example.scaffold.entity.User;
import com.example.scaffold.mapper.UserMapper;
import com.example.scaffold.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
/**
* 用户服务实现 - 演示 Spring 服务层
*
* 学习要点:
* 1. @Service - 标记为服务组件
* 2. @Transactional - 声明式事务
* 3. @RequiredArgsConstructor - Lombok 构造器注入
* 4. @Slf4j - Lombok 日志
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
@Override
public List<User> findAll() {
log.info("📊 [UserService] 查询所有用户");
return userMapper.findAll();
}
@Override
public Optional<User> findById(Long id) {
log.info("🔍 [UserService] 查询用户: id={}", id);
// 演示 MyBatis 一级缓存 - 连续两次查询
User user = userMapper.findById(id);
User cached = userMapper.findById(id); // 第二次会命中缓存
if (cached != null) {
log.info("✅ [UserService] 一级缓存命中: id={}", id);
}
return Optional.ofNullable(user);
}
@Override
@Transactional
public User save(User user) {
log.info("💾 [UserService] 保存用户: {}", user.getUsername());
if (user.getId() == null) {
userMapper.insert(user);
} else {
userMapper.update(user);
}
return user;
}
@Override
@Transactional
public void deleteById(Long id) {
log.info("🗑️ [UserService] 删除用户: id={}", id);
userMapper.deleteById(id);
}
@Override
public List<User> searchByUsername(String username) {
log.info("🔎 [UserService] 搜索用户: username={}", username);
return userMapper.findByUsernameLike(username);
}
@Override
public long count() {
return userMapper.count();
}
}