feat: Spring Boot 学习脚手架 v2.0
- 新增 IoC 容器学习模块 - 新增 AOP 切面编程学习模块 - 新增 MyBatis 集成学习模块 - 新增事务管理学习模块 - 新增用户/产品/订单 CRUD - 新增 7 个交互式学习页面 - 集成性能监控切面
This commit is contained in:
@@ -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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user