feat: Spring Boot 学习脚手架 v2.0
- 新增 IoC 容器学习模块 - 新增 AOP 切面编程学习模块 - 新增 MyBatis 集成学习模块 - 新增事务管理学习模块 - 新增用户/产品/订单 CRUD - 新增 7 个交互式学习页面 - 集成性能监控切面
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
115
src/main/java/com/example/scaffold/aop/LearningAspect.java
Normal file
115
src/main/java/com/example/scaffold/aop/LearningAspect.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
38
src/main/java/com/example/scaffold/config/AppConfig.java
Normal file
38
src/main/java/com/example/scaffold/config/AppConfig.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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!";
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
42
src/main/java/com/example/scaffold/entity/Order.java
Normal file
42
src/main/java/com/example/scaffold/entity/Order.java
Normal 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;
|
||||
}
|
||||
42
src/main/java/com/example/scaffold/entity/Product.java
Normal file
42
src/main/java/com/example/scaffold/entity/Product.java
Normal 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;
|
||||
}
|
||||
101
src/main/java/com/example/scaffold/entity/User.java
Normal file
101
src/main/java/com/example/scaffold/entity/User.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 调用 B,B 加入 A 的事务"
|
||||
),
|
||||
"REQUIRES_NEW", Map.of(
|
||||
"描述", "总是新建事务,挂起当前事务",
|
||||
"场景", "日志记录、独立子任务",
|
||||
"示例", "A 调用 B,B 在新事务执行,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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
33
src/main/java/com/example/scaffold/mapper/OrderMapper.java
Normal file
33
src/main/java/com/example/scaffold/mapper/OrderMapper.java
Normal 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);
|
||||
}
|
||||
40
src/main/java/com/example/scaffold/mapper/ProductMapper.java
Normal file
40
src/main/java/com/example/scaffold/mapper/ProductMapper.java
Normal 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);
|
||||
}
|
||||
78
src/main/java/com/example/scaffold/mapper/UserMapper.java
Normal file
78
src/main/java/com/example/scaffold/mapper/UserMapper.java
Normal 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);
|
||||
}
|
||||
24
src/main/java/com/example/scaffold/service/UserService.java
Normal file
24
src/main/java/com/example/scaffold/service/UserService.java
Normal 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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user