From 2a32df78f67eefe35b65ec7c3a02efeae73eb2ac Mon Sep 17 00:00:00 2001 From: isczj <531285956@qq.com> Date: Sat, 3 Jan 2026 09:02:38 +0800 Subject: [PATCH] =?UTF-8?q?docs(30=E5=A4=A9=E5=AE=9E=E7=8E=B0=E6=8C=87?= =?UTF-8?q?=E5=8D=97):=20=E6=B7=BB=E5=8A=A0=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E8=B7=AF=E7=94=B1SpringBoot=20Starter=E5=AD=A6=E4=B9=A0?= =?UTF-8?q?=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建README.md包含30天学习计划和使用说明 - 添加完整项目代码清单文档 - 创建第01天项目初始化和Maven配置教学文件 - 创建第02天数据库路由和分库分表概念教学文件 - 提供详细的学习路径和实践任务 - 包含知识点拓展和思考题内容 --- .../README.md" | 129 ++++ ...43\347\240\201\346\270\205\345\215\225.md" | 207 ++++++ ...5\222\214Maven\351\205\215\347\275\256.md" | 669 ++++++++++++++++++ ...06\345\272\223\345\210\206\350\241\250.md" | 340 +++++++++ ...50\350\247\243\345\237\272\347\241\200.md" | 308 ++++++++ ...15\345\260\204\345\237\272\347\241\200.md" | 421 +++++++++++ ...251-Spring-AOP\345\237\272\347\241\200.md" | 319 +++++++++ ...15\347\275\256\345\216\237\347\220\206.md" | 330 +++++++++ ...22\344\273\266\346\234\272\345\210\266.md" | 255 +++++++ ...61\345\205\245\347\220\206\350\247\243.md" | 269 +++++++ ...56\346\272\220\345\216\237\347\220\206.md" | 225 ++++++ ...41\345\274\217\350\256\276\350\256\241.md" | 222 ++++++ ...14\345\267\245\345\205\267\347\261\273.md" | 373 ++++++++++ ...57\347\224\261\347\255\226\347\225\245.md" | 236 ++++++ ...07\351\235\242\345\256\236\347\216\260.md" | 262 +++++++ ...226\204MyBatis\346\217\222\344\273\266.md" | 206 ++++++ ...36\347\216\260\346\214\207\345\215\227.md" | 301 ++++++++ ...64\344\273\243\347\240\201\357\274\211.md" | 315 +++++++++ LICENSE | 201 ------ 19 files changed, 5387 insertions(+), 201 deletions(-) create mode 100644 "30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/README.md" create mode 100644 "30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\345\256\214\346\225\264\351\241\271\347\233\256\344\273\243\347\240\201\346\270\205\345\215\225.md" create mode 100644 "30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25401\345\244\251-\351\241\271\347\233\256\345\210\235\345\247\213\345\214\226\345\222\214Maven\351\205\215\347\275\256.md" create mode 100644 "30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25402\345\244\251-\347\220\206\350\247\243\346\225\260\346\215\256\345\272\223\350\267\257\347\224\261\345\222\214\345\210\206\345\272\223\345\210\206\350\241\250.md" create mode 100644 "30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25403\345\244\251-Java\346\263\250\350\247\243\345\237\272\347\241\200.md" create mode 100644 "30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25404\345\244\251-Java\345\217\215\345\260\204\345\237\272\347\241\200.md" create mode 100644 "30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25405\345\244\251-Spring-AOP\345\237\272\347\241\200.md" create mode 100644 "30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25406\345\244\251-Spring-Boot\350\207\252\345\212\250\351\205\215\347\275\256\345\216\237\347\220\206.md" create mode 100644 "30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25407\345\244\251-MyBatis\346\217\222\344\273\266\346\234\272\345\210\266.md" create mode 100644 "30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25408\345\244\251-ThreadLocal\346\267\261\345\205\245\347\220\206\350\247\243.md" create mode 100644 "30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25409\345\244\251-\345\212\250\346\200\201\346\225\260\346\215\256\346\272\220\345\216\237\347\220\206.md" create mode 100644 "30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25410\345\244\251-\347\255\226\347\225\245\346\250\241\345\274\217\350\256\276\350\256\241.md" create mode 100644 "30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25411\345\244\251-\345\256\236\347\216\260\351\205\215\347\275\256\345\261\236\346\200\247\347\261\273\345\222\214\345\267\245\345\205\267\347\261\273.md" create mode 100644 "30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25412\345\244\251-\345\256\236\347\216\260\345\223\210\345\270\214\350\267\257\347\224\261\347\255\226\347\225\245.md" create mode 100644 "30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25413\345\244\251-\345\256\214\345\226\204AOP\345\210\207\351\235\242\345\256\236\347\216\260.md" create mode 100644 "30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25414\345\244\251-\345\256\214\345\226\204MyBatis\346\217\222\344\273\266.md" create mode 100644 "30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25415-30\345\244\251-\345\220\216\347\273\255\345\256\236\347\216\260\346\214\207\345\215\227.md" create mode 100644 "30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25415\345\244\251-\345\256\214\345\226\204\350\207\252\345\212\250\351\205\215\347\275\256\347\261\273\357\274\210\345\256\214\346\225\264\344\273\243\347\240\201\357\274\211.md" delete mode 100644 LICENSE diff --git "a/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/README.md" "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/README.md" new file mode 100644 index 0000000..890320f --- /dev/null +++ "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/README.md" @@ -0,0 +1,129 @@ +# 30天完整实现数据库路由SpringBoot Starter + +## 📚 学习指南 + +本目录包含30天的详细教学文件,每天一个文件,从零开始完整实现 `db-router-spring-boot-starter-1.0.2.jar` 项目。 + +## 📖 文件说明 + +### 已创建的文件 + +✅ **第01-14天**:详细的教学文件,包含完整代码和解释 +- 第01天:项目初始化和Maven配置 +- 第02天:理解数据库路由和分库分表 +- 第03天:Java注解基础 +- 第04天:Java反射基础 +- 第05天:Spring AOP基础 +- 第06天:Spring Boot自动配置原理 +- 第07天:MyBatis插件机制 +- 第08天:ThreadLocal深入理解 +- 第09天:动态数据源原理 +- 第10天:策略模式设计 +- 第11天:实现配置属性类和工具类 +- 第12天:实现哈希路由策略 +- 第13天:完善AOP切面实现 +- 第14天:完善MyBatis插件 + +✅ **第15-30天**:实现指南大纲(见 `第15-30天-后续实现指南.md`) + +✅ **完整项目代码清单**:所有文件的清单和实现顺序(见 `完整项目代码清单.md`) + +## 🎯 学习方式 + +1. **按顺序学习**:从第01天开始,每天完成一个文件的学习和实现 +2. **动手实践**:每学一个知识点,立即写代码验证 +3. **理解原理**:不仅要会写,更要理解为什么这样写 +4. **拓展思考**:每个知识点都有拓展内容,多思考多实践 +5. **记录笔记**:记录学习心得和遇到的问题 + +## 📝 使用说明 + +### 第一步:阅读学习计划 + +先阅读 `学习计划-数据库路由SpringBootStarter实现.md`,了解整体学习路线。 + +### 第二步:按天学习 + +1. 每天打开对应的文件(如:`第01天-项目初始化和Maven配置.md`) +2. 阅读"今日目标"和"知识点"部分 +3. 按照"实践任务"中的步骤操作 +4. 复制代码到你的项目中(**不要只是复制,要理解每行代码**) +5. 运行代码,验证功能 +6. 阅读"知识点拓展"部分,加深理解 +7. 完成"思考题" +8. 在"检查清单"中打勾 + +### 第三步:完成项目 + +1. 按照 `完整项目代码清单.md` 检查所有文件是否完成 +2. 参考 `第15-30天-后续实现指南.md` 完成剩余功能 +3. 编写测试,验证功能 +4. 编写文档 + +## 📋 文件结构 + +每个教学文件都包含: +- 📚 **今日目标**:今天要学什么 +- 🎯 **知识点**:理论知识讲解 +- 🛠️ **实践任务**:动手实现(含完整代码) +- 🎓 **知识点拓展**:深入理解,举一反三 +- ✅ **检查清单**:验证是否完成 +- 🎯 **明日预告**:明天学什么 +- 💡 **思考题**:加深理解 +- 📚 **参考资源**:扩展阅读 + +## ⚠️ 重要提示 + +1. **不要只是复制代码**:理解每行代码的含义 +2. **遇到问题先思考**:自己思考10分钟,再查资料 +3. **记录问题**:把遇到的问题记录下来,逐步解决 +4. **完成拓展练习**:拓展内容很重要,不要跳过 +5. **循序渐进**:不要急于求成,每天完成当天的任务即可 + +## ✅ 检查清单 + +完成每天的学习后: +- [ ] 阅读了知识点部分 +- [ ] 完成了实践任务 +- [ ] 理解了代码含义 +- [ ] 完成了拓展阅读 +- [ ] 完成了思考题 +- [ ] 在文件末尾的检查清单中打勾 + +## 🚀 开始学习 + +### 推荐学习路径 + +1. **第1步**:阅读 `学习计划-数据库路由SpringBootStarter实现.md` +2. **第2步**:阅读 `完整项目代码清单.md`,了解整体结构 +3. **第3步**:从 `第01天-项目初始化和Maven配置.md` 开始学习 +4. **第4步**:按顺序完成第01-14天的学习 +5. **第5步**:参考 `第15-30天-后续实现指南.md` 完成剩余功能 + +### 学习时间建议 + +- **每天学习时间**:2-3小时 +- **理论学习**:30-40% +- **编码实践**:50-60% +- **总结反思**:10% + +## 💡 遇到问题怎么办? + +1. **先思考**:自己先思考10分钟 +2. **查文档**:查阅相关文档(每个文件末尾都有参考资源) +3. **看源码**:看Spring、MyBatis的源码 +4. **问问题**:在技术社区提问 +5. **调试**:使用调试工具逐步排查 + +## 🎉 完成后的收获 + +完成30天的学习后,你将: +- ✅ 完全理解数据库路由的原理 +- ✅ 掌握Spring Boot Starter的开发 +- ✅ 理解AOP、反射、注解等核心概念 +- ✅ 能够独立开发类似的中间件 +- ✅ 知其然,知其所以然 + +--- + +**祝你学习顺利!有问题随时记录,每天进步一点点!** 🎉 diff --git "a/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\345\256\214\346\225\264\351\241\271\347\233\256\344\273\243\347\240\201\346\270\205\345\215\225.md" "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\345\256\214\346\225\264\351\241\271\347\233\256\344\273\243\347\240\201\346\270\205\345\215\225.md" new file mode 100644 index 0000000..05040e8 --- /dev/null +++ "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\345\256\214\346\225\264\351\241\271\347\233\256\344\273\243\347\240\201\346\270\205\345\215\225.md" @@ -0,0 +1,207 @@ +# 完整项目代码清单 + +## 📚 说明 + +本文档列出了实现 `db-router-spring-boot-starter` 项目所需的所有代码文件。按照30天的学习计划,逐步实现这些文件。 + +--- + +## 📁 项目结构 + +``` +db-router-spring-boot-starter/ +├── pom.xml +├── src/ +│ ├── main/ +│ │ ├── java/ +│ │ │ └── cn/bugstack/middleware/db/router/ +│ │ │ ├── annotation/ +│ │ │ │ ├── DBRouter.java +│ │ │ │ └── DBRouterStrategy.java +│ │ │ ├── config/ +│ │ │ │ └── DataSourceAutoConfig.java +│ │ │ ├── dynamic/ +│ │ │ │ ├── DynamicDataSource.java +│ │ │ │ └── DynamicMybatisPlugin.java +│ │ │ ├── strategy/ +│ │ │ │ ├── IDBRouterStrategy.java +│ │ │ │ └── impl/ +│ │ │ │ └── DBRouterStrategyHashCode.java +│ │ │ ├── util/ +│ │ │ │ ├── StringUtils.java +│ │ │ │ └── PropertyUtil.java +│ │ │ ├── DBContextHolder.java +│ │ │ ├── DBRouterBase.java +│ │ │ ├── DBRouterConfig.java +│ │ │ └── DBRouterJoinPoint.java +│ │ └── resources/ +│ │ └── META-INF/ +│ │ └── spring.factories +│ └── test/ +│ └── java/ +``` + +--- + +## 📝 文件清单 + +### 1. pom.xml +**位置**:项目根目录 +**说明**:Maven配置文件,包含所有依赖 +**实现日期**:第01天 + +### 2. DBRouter.java +**位置**:`src/main/java/cn/bugstack/middleware/db/router/annotation/` +**说明**:数据库路由注解 +**实现日期**:第03天 + +### 3. DBRouterStrategy.java +**位置**:`src/main/java/cn/bugstack/middleware/db/router/annotation/` +**说明**:数据库路由策略注解 +**实现日期**:第03天 + +### 4. PropertyUtil.java +**位置**:`src/main/java/cn/bugstack/middleware/db/router/util/` +**说明**:属性工具类,用于反射获取属性值 +**实现日期**:第04天 + +### 5. StringUtils.java +**位置**:`src/main/java/cn/bugstack/middleware/db/router/util/` +**说明**:字符串工具类 +**实现日期**:第11天 + +### 6. DBRouterConfig.java +**位置**:`src/main/java/cn/bugstack/middleware/db/router/` +**说明**:数据库路由配置类 +**实现日期**:第11天 + +### 7. IDBRouterStrategy.java +**位置**:`src/main/java/cn/bugstack/middleware/db/router/strategy/` +**说明**:路由策略接口 +**实现日期**:第10天 + +### 8. DBRouterStrategyHashCode.java +**位置**:`src/main/java/cn/bugstack/middleware/db/router/strategy/impl/` +**说明**:哈希路由策略实现 +**实现日期**:第12天 + +### 9. DBContextHolder.java +**位置**:`src/main/java/cn/bugstack/middleware/db/router/` +**说明**:数据库路由上下文持有者,使用ThreadLocal存储路由信息 +**实现日期**:第08天 + +### 10. DBRouterBase.java +**位置**:`src/main/java/cn/bugstack/middleware/db/router/` +**说明**:数据库路由基类 +**实现日期**:第12天 + +### 11. DynamicDataSource.java +**位置**:`src/main/java/cn/bugstack/middleware/db/router/dynamic/` +**说明**:动态数据源,继承AbstractRoutingDataSource +**实现日期**:第09天 + +### 12. DynamicMybatisPlugin.java +**位置**:`src/main/java/cn/bugstack/middleware/db/router/dynamic/` +**说明**:MyBatis插件,用于修改SQL中的表名 +**实现日期**:第07天、第14天 + +### 13. DBRouterJoinPoint.java +**位置**:`src/main/java/cn/bugstack/middleware/db/router/` +**说明**:AOP切面,拦截带@DBRouter注解的方法 +**实现日期**:第05天、第13天 + +### 14. DataSourceAutoConfig.java +**位置**:`src/main/java/cn/bugstack/middleware/db/router/config/` +**说明**:数据源自动配置类 +**实现日期**:第06天、第15天 + +### 15. spring.factories +**位置**:`src/main/resources/META-INF/` +**说明**:Spring Boot自动配置入口文件 +**实现日期**:第16天 + +--- + +## 🔄 实现顺序 + +### 第一阶段:基础知识(第01-10天) +1. 项目初始化(pom.xml) +2. 理解数据库路由概念 +3. 实现注解(DBRouter、DBRouterStrategy) +4. 实现工具类(PropertyUtil) +5. 理解AOP和实现切面(DBRouterJoinPoint) +6. 理解自动配置(DataSourceAutoConfig) +7. 实现MyBatis插件(DynamicMybatisPlugin) +8. 实现上下文持有者(DBContextHolder) +9. 实现动态数据源(DynamicDataSource) +10. 设计策略接口(IDBRouterStrategy) + +### 第二阶段:核心实现(第11-20天) +11. 实现配置类和工具类(DBRouterConfig、StringUtils) +12. 实现路由策略(DBRouterStrategyHashCode、DBRouterBase) +13. 完善AOP切面(DBRouterJoinPoint) +14. 完善MyBatis插件(DynamicMybatisPlugin) +15. 完善自动配置(DataSourceAutoConfig) +16. 创建spring.factories +17-20. 测试和优化 + +### 第三阶段:功能完善(第21-30天) +21-30. 功能完善、测试、文档 + +--- + +## 📋 检查清单 + +完成每个文件后,在对应日期打勾: + +### 注解 +- [ ] DBRouter.java(第03天) +- [ ] DBRouterStrategy.java(第03天) + +### 配置 +- [ ] DBRouterConfig.java(第11天) +- [ ] DataSourceAutoConfig.java(第06天、第15天) + +### 核心类 +- [ ] DBContextHolder.java(第08天) +- [ ] DBRouterJoinPoint.java(第05天、第13天) +- [ ] DBRouterBase.java(第12天) + +### 动态数据源 +- [ ] DynamicDataSource.java(第09天) +- [ ] DynamicMybatisPlugin.java(第07天、第14天) + +### 策略 +- [ ] IDBRouterStrategy.java(第10天) +- [ ] DBRouterStrategyHashCode.java(第12天) + +### 工具类 +- [ ] StringUtils.java(第11天) +- [ ] PropertyUtil.java(第04天) + +### 配置文件 +- [ ] pom.xml(第01天) +- [ ] spring.factories(第16天) + +--- + +## 🎯 使用说明 + +1. **按顺序学习**:从第01天开始,每天完成一个文件 +2. **理解原理**:不要只是复制代码,要理解为什么这样写 +3. **动手实践**:每学一个知识点就写代码验证 +4. **记录问题**:遇到问题记录下来,逐步解决 +5. **拓展思考**:完成拓展练习,加深理解 + +--- + +## 💡 提示 + +- 所有代码都在对应的日期文件中 +- 每个文件都有详细的代码和解释 +- 遇到问题可以查看对应日期的文件 +- 完成所有文件后,项目就可以使用了 + +--- + +**祝你学习顺利!** 🎉 diff --git "a/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25401\345\244\251-\351\241\271\347\233\256\345\210\235\345\247\213\345\214\226\345\222\214Maven\351\205\215\347\275\256.md" "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25401\345\244\251-\351\241\271\347\233\256\345\210\235\345\247\213\345\214\226\345\222\214Maven\351\205\215\347\275\256.md" new file mode 100644 index 0000000..6abc3ad --- /dev/null +++ "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25401\345\244\251-\351\241\271\347\233\256\345\210\235\345\247\213\345\214\226\345\222\214Maven\351\205\215\347\275\256.md" @@ -0,0 +1,669 @@ +# 第01天:项目初始化和Maven配置 + +## 📚 今日目标 + +1. 理解什么是Spring Boot +2. 理解什么是Spring Boot Starter +3. 创建Maven项目结构 +4. 配置pom.xml文件 +5. 理解项目依赖关系 + +--- + +## 🎯 知识点0:什么是Spring Boot?(先理解这个) + +### 生活中的例子 + +**传统Java开发 vs Spring Boot开发** + +**传统方式(复杂)**: +``` +想象你要做一道菜: +1. 买锅(配置Tomcat服务器) +2. 买调料(配置各种XML文件) +3. 准备食材(写很多配置代码) +4. 生火(启动服务器) +5. 炒菜(写业务代码) +``` + +**Spring Boot方式(简单)**: +``` +Spring Boot就像"外卖套餐": +1. 打开包装(引入依赖) +2. 加热(运行main方法) +3. 开吃(直接写业务代码) +``` + +### 最简单的例子 + +**传统Spring项目**(需要配置很多XML): + +```java +// 1. 需要web.xml配置 +// 2. 需要applicationContext.xml配置 +// 3. 需要配置数据源、事务等 +// 4. 需要部署到Tomcat服务器 +// ... 很多配置 +``` + +**Spring Boot项目**(几乎零配置): + +```java +// 1. 创建一个类 +@SpringBootApplication +public class MyApp { + public static void main(String[] args) { + SpringApplication.run(MyApp.class, args); + } +} + +// 2. 运行main方法 +// 3. 完成!服务器自动启动,可以访问了 +``` + +### Spring Boot的核心特点 + +**1. 自动配置(Auto Configuration)** +``` +传统方式:需要手动配置每个组件 +Spring Boot:检测到classpath中的类,自动配置 +``` + +**例子**: +```java +// 传统方式:需要手动配置数据源 +@Configuration +public class DataSourceConfig { + @Bean + public DataSource dataSource() { + // 写很多配置代码 + } +} + +// Spring Boot:引入依赖,自动配置 +// 只需要在application.yml中写: +spring: + datasource: + url: jdbc:mysql://localhost:3306/test + username: root + password: 123456 +// 数据源自动创建好了! +``` + +**2. 起步依赖(Starter Dependencies)** +``` +传统方式:需要一个个引入依赖,还要处理版本冲突 +Spring Boot:引入一个starter,自动引入所有相关依赖 +``` + +**例子**: +```xml + + + org.springframework + spring-web + + + org.springframework + spring-webmvc + + + + + + org.springframework.boot + spring-boot-starter-web + + +``` + +**3. 内嵌服务器(Embedded Server)** +``` +传统方式:需要安装Tomcat,打包成war,部署到服务器 +Spring Boot:内嵌Tomcat,打包成jar,直接运行 +``` + +**例子**: +```bash +# 传统方式 +1. 打包成war文件 +2. 部署到Tomcat +3. 启动Tomcat +4. 访问应用 + +# Spring Boot方式 +1. 打包成jar文件 +2. java -jar app.jar +3. 完成!直接访问 +``` + +### 最简单的完整例子 + +**创建一个Web接口**: + +```java +// 1. 主类 +@SpringBootApplication +public class HelloApp { + public static void main(String[] args) { + SpringApplication.run(HelloApp.class, args); + } +} + +// 2. 控制器(处理HTTP请求) +@RestController +public class HelloController { + @GetMapping("/hello") + public String hello() { + return "Hello Spring Boot!"; + } +} + +// 3. 运行main方法 +// 4. 浏览器访问:http://localhost:8080/hello +// 5. 看到:Hello Spring Boot! +``` + +**就这么简单!** +- 不需要配置web.xml +- 不需要配置Tomcat +- 不需要部署 +- 直接运行,直接访问 + +### 为什么用Spring Boot? + +**传统Spring的问题**: +- ❌ 配置复杂(XML、Java配置) +- ❌ 依赖管理困难(版本冲突) +- ❌ 部署复杂(需要服务器) +- ❌ 开发效率低 + +**Spring Boot的优势**: +- ✅ 零配置(约定优于配置) +- ✅ 自动配置(开箱即用) +- ✅ 内嵌服务器(直接运行) +- ✅ 开发效率高 + +### 总结 + +**Spring Boot = Spring框架 + 自动配置 + 起步依赖 + 内嵌服务器** + +**核心思想**:**约定优于配置** +- 不需要配置的,就不配置 +- 有默认值的,就用默认值 +- 需要配置的,才配置 + +--- + +## 🎯 知识点1:什么是Spring Boot Starter? + +### 为什么需要Starter? + +**问题场景**: +- 每次使用某个框架(如MyBatis),都要手动配置很多Bean +- 配置复杂,容易出错 +- 不同项目重复配置 + +**解决方案**:Spring Boot Starter +- 把常用配置打包成一个依赖 +- 引入依赖后自动配置 +- 开箱即用,零配置 + +**例子**: +```xml + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + + + +``` + +### 我们的目标 + +创建一个 `db-router-spring-boot-starter`,让用户: +1. 引入依赖 +2. 配置几个参数 +3. 使用 `@DBRouter` 注解 +4. 自动实现分库分表 + +--- + +## 🎯 知识点2:Maven项目结构 + +### 标准Maven项目结构 + +``` +db-router-spring-boot-starter/ +├── pom.xml # Maven配置文件 +├── src/ +│ ├── main/ +│ │ ├── java/ # Java源代码 +│ │ │ └── cn/bugstack/middleware/db/router/ +│ │ │ ├── annotation/ # 注解 +│ │ │ ├── config/ # 配置类 +│ │ │ ├── dynamic/ # 动态数据源 +│ │ │ ├── strategy/ # 路由策略 +│ │ │ └── util/ # 工具类 +│ │ └── resources/ +│ │ └── META-INF/ +│ │ └── spring.factories # 自动配置入口 +│ └── test/ # 测试代码 +│ └── java/ +``` + +### 为什么这样组织? + +- **annotation**:存放自定义注解(@DBRouter等) +- **config**:Spring配置类(自动配置) +- **dynamic**:动态数据源相关类 +- **strategy**:路由策略(策略模式) +- **util**:工具类(字符串、属性等) + +--- + +## 🛠️ 实践任务1:创建项目结构 + +### 步骤1:创建目录 + +在你的项目根目录执行: + +```bash +mkdir -p src/main/java/cn/bugstack/middleware/db/router/{annotation,config,dynamic,strategy/impl,util} +mkdir -p src/main/resources/META-INF +mkdir -p src/test/java +``` + +### 步骤2:验证结构 + +```bash +tree src/ # 如果没有tree命令,用 find src -type d +``` + +应该看到: +``` +src/ +├── main +│ ├── java +│ │ └── cn +│ │ └── bugstack +│ │ └── middleware +│ │ └── db +│ │ └── router +│ │ ├── annotation +│ │ ├── config +│ │ ├── dynamic +│ │ ├── strategy +│ │ │ └── impl +│ │ └── util +│ └── resources +│ └── META-INF +└── test + └── java +``` + +--- + +## 🛠️ 实践任务2:创建pom.xml + +### 完整pom.xml代码 + +在项目根目录创建 `pom.xml`: + +```xml + + + 4.0.0 + + + cn.bugstack.middleware + db-router-spring-boot-starter + 1.0.2 + jar + + + + org.springframework.boot + spring-boot-starter-parent + 2.3.5.RELEASE + + + + + UTF-8 + 1.8 + 1.8 + + + + + + org.springframework.boot + spring-boot-starter + + + + + org.springframework.boot + spring-boot-autoconfigure + + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 2.1.4 + + + + + + + + + + commons-beanutils + commons-beanutils + 1.9.4 + + + + + + commons-lang + commons-lang + 2.6 + + + + + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + db-router-spring-boot-starter + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + UTF-8 + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar + + + + + + + +``` + +### 依赖说明 + +**📋 依赖分类总结**: + +**✅ 必须依赖(核心功能需要)**: +1. spring-boot-starter - Spring Boot核心 +2. spring-boot-autoconfigure - 自动配置 +3. spring-boot-starter-aop - AOP切面 +4. mybatis-spring-boot-starter - MyBatis插件 +5. commons-beanutils - 反射获取属性 +6. commons-lang - 字符串工具 + +**❌ 可选依赖(已注释,可删除)**: +1. mysql-connector-java - 用户项目会自己引入 +2. fastjson - 代码中没有使用 +3. junit - spring-boot-starter-test已包含 + +**⚠️ 可选但建议保留**: +1. spring-boot-configuration-processor - IDE提示(optional=true,不影响运行时) +2. spring-boot-starter-test - 测试用(scope=test,不影响打包) + +**📝 关于HikariCP的说明**: +- 第15天的DataSourceAutoConfig代码中使用了HikariCP创建数据源 +- 但pom.xml中没有HikariCP依赖,因为: + 1. 用户项目通常会引入spring-boot-starter-jdbc(已包含HikariCP) + 2. 或者用户自己选择连接池(Druid、HikariCP等) + 3. 我们的starter不应该强制指定连接池 +- 如果测试时需要,可以临时添加: + ```xml + + com.zaxxer + HikariCP + + ``` + +--- + +#### 1. Spring Boot Starter +```xml + + org.springframework.boot + spring-boot-starter + +``` +**作用**:Spring Boot核心功能,包含自动配置、日志等 + +#### 2. Spring Boot Autoconfigure +```xml + + org.springframework.boot + spring-boot-autoconfigure + +``` +**作用**:自动配置的核心,我们用它来实现自动配置 + +#### 3. Spring Boot AOP +```xml + + org.springframework.boot + spring-boot-starter-aop + +``` +**作用**:AOP功能,用于拦截方法,实现路由逻辑 + +#### 4. MyBatis Spring Boot Starter +```xml + + org.mybatis.spring.boot + mybatis-spring-boot-starter + +``` +**作用**:MyBatis集成,我们需要拦截SQL修改表名 + +#### 5. Commons BeanUtils +```xml + + commons-beanutils + commons-beanutils + +``` +**作用**:通过反射获取对象属性值(如获取userId) +**是否必须**:✅ 必须 - PropertyUtil.getProperty()方法使用 + +#### 6. Commons Lang +```xml + + commons-lang + commons-lang + +``` +**作用**:字符串工具类(isBlank等方法) +**是否必须**:✅ 必须 - StringUtils使用 + +#### 7. MySQL驱动 +**是否必须**:❌ 可选 - 用户项目会自己引入数据库驱动 + +#### 8. FastJSON +**是否必须**:❌ 可选 - 代码中没有使用,可以删除 + +#### 9. 测试依赖 +**是否必须**:❌ 可选 - 只在测试时需要(scope=test) + +--- + +## 🎓 知识点拓展 + +### 拓展1:Maven依赖作用域(Scope) + +```xml +compile +provided +runtime +test +system +``` + +**为什么spring-boot-configuration-processor用optional?** +- `optional=true` 表示这个依赖不会传递 +- 只在开发时用于IDE提示,运行时不需要 +- 避免用户项目引入不必要的依赖 + +### 拓展2:Spring Boot版本选择 + +**为什么用2.3.5?** +- 这是原项目使用的版本 +- 2.x版本稳定,兼容性好 +- 3.x版本需要Java 17+,门槛更高 + +**如何选择版本?** +- 生产环境:选择稳定版本(如2.7.x) +- 学习环境:可以尝试最新版本 +- 注意:不同版本API可能有差异 + +### 拓展3:Maven坐标(Coordinates) + +```xml +cn.bugstack.middleware +db-router-spring-boot-starter +1.0.2 +``` + +**命名规范**: +- groupId:通常是域名倒写(如com.company.project) +- artifactId:项目名,小写,用连字符 +- version:语义化版本(主版本.次版本.修订版本) + +--- + +## ✅ 今日检查清单 + +- [ ] 创建了完整的项目目录结构 +- [ ] 创建了pom.xml文件 +- [ ] 理解了每个依赖的作用 +- [ ] 能够解释为什么需要这些依赖 +- [ ] 完成了拓展阅读 + +--- + +## 🎯 明日预告 + +明天我们将学习: +- 什么是数据库路由 +- 为什么需要分库分表 +- 路由的基本原理 + +--- + +## 💡 思考题 + +1. 如果不用Spring Boot Starter,用户需要手动配置哪些东西? +2. 为什么starter的依赖要用`optional=true`? +3. Maven的``标签有什么作用? + +--- + +## 📚 参考资源 + +- [Spring Boot官方文档](https://spring.io/projects/spring-boot) +- [Maven官方文档](https://maven.apache.org/guides/) +- [Spring Boot Starter开发指南](https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.build-systems.starters) diff --git "a/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25402\345\244\251-\347\220\206\350\247\243\346\225\260\346\215\256\345\272\223\350\267\257\347\224\261\345\222\214\345\210\206\345\272\223\345\210\206\350\241\250.md" "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25402\345\244\251-\347\220\206\350\247\243\346\225\260\346\215\256\345\272\223\350\267\257\347\224\261\345\222\214\345\210\206\345\272\223\345\210\206\350\241\250.md" new file mode 100644 index 0000000..c71d224 --- /dev/null +++ "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25402\345\244\251-\347\220\206\350\247\243\346\225\260\346\215\256\345\272\223\350\267\257\347\224\261\345\222\214\345\210\206\345\272\223\345\210\206\350\241\250.md" @@ -0,0 +1,340 @@ +# 第02天:理解数据库路由和分库分表 + +## 📚 今日目标 + +1. 理解什么是数据库路由 +2. 理解为什么需要分库分表 +3. 理解路由的基本原理 +4. 画图理解分库分表架构 + +--- + +## 🎯 知识点1:什么是数据库路由? + +### 生活中的例子 + +**快递分拣**: +- 快递员根据地址,把包裹送到不同的分拣中心 +- 北京 → 北京分拣中心 +- 上海 → 上海分拣中心 + +**数据库路由**: +- 根据路由键(如用户ID),把数据存到不同的数据库/表 +- 用户ID 1-1000万 → 数据库1,表1 +- 用户ID 1000万-2000万 → 数据库2,表2 + +### 核心概念 + +**路由键(Router Key)**: +- 用来决定数据存在哪里的字段 +- 通常是业务主键(如用户ID、订单ID) +- 必须保证:相同路由键 → 相同库表 + +**路由算法**: +- 根据路由键计算目标库和表 +- 常用算法:哈希、取模、范围等 + +--- + +## 🎯 知识点2:为什么需要分库分表? + +### 问题场景 + +**单库单表的问题**: + +``` +用户表(user) +├── 1亿条数据 +├── 查询越来越慢 +├── 索引越来越大 +└── 数据库压力巨大 +``` + +**性能瓶颈**: +1. **查询慢**:数据量大,即使有索引也很慢 +2. **写入慢**:插入数据需要维护索引 +3. **锁竞争**:高并发时锁竞争激烈 +4. **单点故障**:一个数据库挂了,整个系统不可用 + +### 解决方案:分库分表 + +**分表(水平分表)**: +``` +原来:user(1亿条) +现在: +├── user_01(2500万条) +├── user_02(2500万条) +├── user_03(2500万条) +└── user_04(2500万条) +``` + +**分库(水平分库)**: +``` +原来:db1(1亿条) +现在: +├── db1(2500万条) +├── db2(2500万条) +├── db3(2500万条) +└── db4(2500万条) +``` + +**效果**: +- 单表数据量减少 → 查询更快 +- 分散到多个库 → 并发能力提升 +- 单个库故障 → 不影响其他库 + +--- + +## 🎯 知识点3:路由算法 + +### 算法1:哈希路由 + +**原理**: +```java +int dbIndex = Math.abs(userId.hashCode()) % dbCount; +int tbIndex = Math.abs(userId.hashCode()) % tbCount; +``` + +**例子**: +``` +userId = "12345678" +hashCode = 12345678.hashCode() = 12345678 +dbCount = 2 +tbCount = 4 + +dbIndex = 12345678 % 2 = 0 → db01 +tbIndex = 12345678 % 4 = 2 → user_02 +``` + +**优点**: +- 数据分布均匀 +- 相同userId总是路由到相同位置 + +**缺点**: +- 扩容困难(需要重新分布数据) + +### 算法2:取模路由 + +**原理**: +```java +int dbIndex = userId % dbCount; +int tbIndex = userId % tbCount; +``` + +**例子**: +``` +userId = 12345678 +dbCount = 2 +tbCount = 4 + +dbIndex = 12345678 % 2 = 0 → db01 +tbIndex = 12345678 % 4 = 2 → user_02 +``` + +**优点**: +- 简单直接 +- 计算快速 + +**缺点**: +- 只适用于数字类型路由键 +- 扩容困难 + +### 算法3:范围路由 + +**原理**: +```java +if (userId >= 1 && userId <= 10000000) { + dbIndex = 0; // db01 +} else if (userId > 10000000 && userId <= 20000000) { + dbIndex = 1; // db02 +} +``` + +**优点**: +- 扩容相对容易(只需添加新范围) +- 查询范围数据方便 + +**缺点**: +- 数据分布可能不均匀 +- 需要维护范围配置 + +--- + +## 🛠️ 实践任务:画图理解架构 + +### 任务1:画分库分表架构图 + +用纸笔或画图工具画出: + +``` +用户请求(userId=12345678) + ↓ +路由计算 + ↓ +dbIndex = 0, tbIndex = 2 + ↓ +选择数据源:db01 + ↓ +修改SQL:user → user_02 + ↓ +执行SQL +``` + +### 任务2:理解数据分布 + +假设: +- 2个数据库(db01, db02) +- 每个库4张表(user_01, user_02, user_03, user_04) + +**问题**: +1. userId=1 应该存在哪里? +2. userId=10000001 应该存在哪里? +3. 如何保证相同userId总是路由到相同位置? + +**答案**: +``` +userId=1: + hashCode = 1 + dbIndex = 1 % 2 = 1 → db02 + tbIndex = 1 % 4 = 1 → user_01 + 结果:db02.user_01 + +userId=10000001: + hashCode = 10000001 + dbIndex = 10000001 % 2 = 1 → db02 + tbIndex = 10000001 % 4 = 1 → user_01 + 结果:db02.user_01 +``` + +--- + +## 🎓 知识点拓展 + +### 拓展1:垂直分表 vs 水平分表 + +**垂直分表**: +``` +原来:user表 +├── id +├── name +├── age +├── email +├── address +└── description(大字段) + +现在: +├── user_base(id, name, age) +└── user_detail(id, email, address, description) +``` +**场景**:某些字段很大但不常用 + +**水平分表**: +``` +原来:user(1亿条) +现在: +├── user_01(2500万条) +├── user_02(2500万条) +├── user_03(2500万条) +└── user_04(2500万条) +``` +**场景**:数据量大,需要分散 + +### 拓展2:分库分表的挑战 + +**1. 跨库查询** +``` +问题:查询所有订单总金额 +原来:SELECT SUM(amount) FROM order +现在:需要查询多个库,然后汇总 +``` + +**解决方案**: +- 避免跨库查询(设计时考虑) +- 使用中间件(如ShardingSphere) +- 数据汇总表 + +**2. 分布式事务** +``` +问题:用户下单需要: +1. 扣减库存(db1) +2. 创建订单(db2) +3. 扣减余额(db3) +如何保证原子性? +``` + +**解决方案**: +- 避免跨库事务(尽量单库完成) +- 使用分布式事务(如Seata) +- 最终一致性(消息队列) + +**3. 扩容** +``` +问题:2个库不够用了,要扩容到4个库 +如何迁移数据? +``` + +**解决方案**: +- 双写方案(新旧库同时写) +- 数据迁移工具 +- 平滑扩容 + +### 拓展3:路由键选择 + +**好的路由键**: +- ✅ 业务主键(用户ID、订单ID) +- ✅ 分布均匀 +- ✅ 查询时经常用到 + +**不好的路由键**: +- ❌ 时间戳(数据倾斜) +- ❌ 随机值(无法定位) +- ❌ 很少查询的字段 + +**例子**: +``` +订单表路由键选择: + +✅ 用户ID(userId) + - 用户查询自己的订单 + - 数据分布均匀 + +❌ 订单时间(createTime) + - 最近的数据都在一个库 + - 数据倾斜严重 +``` + +--- + +## ✅ 今日检查清单 + +- [ ] 理解了数据库路由的概念 +- [ ] 理解了为什么需要分库分表 +- [ ] 理解了路由算法的原理 +- [ ] 画出了分库分表架构图 +- [ ] 完成了拓展阅读 +- [ ] 思考了路由键的选择 + +--- + +## 🎯 明日预告 + +明天我们将学习: +- Java注解(Annotation)基础 +- 如何自定义注解 +- 注解的元注解 + +--- + +## 💡 思考题 + +1. 如果路由键是字符串(如手机号),如何计算路由? +2. 分库分表后,如何保证全局ID唯一? +3. 如果某个库的数据量特别大,怎么办? + +--- + +## 📚 参考资源 + +- [分库分表原理](https://www.cnblogs.com/littlecharacter/p/9342369.html) +- [数据库分片策略](https://shardingsphere.apache.org/document/current/cn/features/sharding/concept/sharding/) +- [分布式系统设计](https://github.com/donnemartin/system-design-primer) diff --git "a/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25403\345\244\251-Java\346\263\250\350\247\243\345\237\272\347\241\200.md" "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25403\345\244\251-Java\346\263\250\350\247\243\345\237\272\347\241\200.md" new file mode 100644 index 0000000..f5374b8 --- /dev/null +++ "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25403\345\244\251-Java\346\263\250\350\247\243\345\237\272\347\241\200.md" @@ -0,0 +1,308 @@ +# 第03天:Java注解基础 + +## 📚 今日目标 + +1. 理解Java注解的概念 +2. 学会自定义注解 +3. 理解注解的元注解 +4. 实现@DBRouter注解 + +--- + +## 🎯 知识点1:什么是注解? + +### 生活中的例子 + +**标签**: +- 商品上的标签:价格、产地、保质期 +- 代码上的标签:@Override、@Deprecated + +**注解的作用**: +- 给代码添加元数据(metadata) +- 告诉编译器、框架如何处理代码 +- 运行时可以通过反射读取 + +### Java内置注解 + +```java +@Override // 标记方法重写父类方法 +@Deprecated // 标记方法已过时 +@SuppressWarnings("unchecked") // 抑制警告 +``` + +--- + +## 🎯 知识点2:注解的元注解 + +### 元注解(Meta-Annotation) + +**定义**:用来定义注解的注解 + +### @Target:指定注解可以用在哪里 + +```java +@Target(ElementType.METHOD) // 只能用在方法上 +@Target(ElementType.TYPE) // 只能用在类上 +@Target({ElementType.METHOD, ElementType.TYPE}) // 可以用在方法和类上 +``` + +**ElementType枚举值**: +- `TYPE`:类、接口、枚举 +- `METHOD`:方法 +- `FIELD`:字段 +- `PARAMETER`:参数 +- `CONSTRUCTOR`:构造函数 +- `LOCAL_VARIABLE`:局部变量 +- `ANNOTATION_TYPE`:注解类型 +- `PACKAGE`:包 + +### @Retention:指定注解保留到什么时候 + +```java +@Retention(RetentionPolicy.SOURCE) // 只在源码中,编译后丢弃 +@Retention(RetentionPolicy.CLASS) // 编译到class文件,运行时不可用 +@Retention(RetentionPolicy.RUNTIME) // 运行时可用(可以通过反射读取) +``` + +**为什么用RUNTIME?** +- 我们需要在运行时读取注解 +- 通过反射获取注解信息 +- 根据注解信息执行路由逻辑 + +### @Documented:生成JavaDoc + +```java +@Documented // 注解信息会包含在JavaDoc中 +``` + +### @Inherited:可以继承 + +```java +@Inherited // 子类会继承父类的注解 +``` + +--- + +## 🛠️ 实践任务1:创建@DBRouter注解 + +### 步骤1:创建注解文件 + +在 `src/main/java/cn/bugstack/middleware/db/router/annotation/` 目录下创建 `DBRouter.java`: + +```java +package cn.bugstack.middleware.db.router.annotation; + +import java.lang.annotation.*; + +/** + * 数据库路由注解 + * + * 使用方式: + * @DBRouter(key = "userId") + * public void queryUser(User user) { + * // 方法实现 + * } + * + * @author 小傅哥 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface DBRouter { + + /** + * 分库分表字段 + * 用于指定路由键,如:userId、orderId等 + * + * @return 路由键字段名 + */ + String key() default ""; +} +``` + +### 代码解释 + +1. **@Documented**:生成JavaDoc时会包含这个注解 +2. **@Retention(RetentionPolicy.RUNTIME)**:运行时可用,可以通过反射读取 +3. **@Target({ElementType.METHOD, ElementType.TYPE})**:可以用在方法和类上 +4. **String key() default ""**:注解属性,默认值为空字符串 + +### 使用示例 + +```java +// 方法级别使用 +@DBRouter(key = "userId") +public User queryUser(User user) { + return userMapper.selectById(user.getUserId()); +} + +// 类级别使用 +@DBRouter(key = "userId") +public class UserService { + // ... +} +``` + +--- + +## 🛠️ 实践任务2:创建@DBRouterStrategy注解 + +### 步骤1:创建注解文件 + +在同一个目录下创建 `DBRouterStrategy.java`: + +```java +package cn.bugstack.middleware.db.router.annotation; + +import java.lang.annotation.*; + +/** + * 数据库路由策略注解 + * + * 用于类级别,指定是否分表 + * + * @author 小傅哥 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DBRouterStrategy { + + /** + * 是否分表 + * true:需要分表,SQL中的表名会被替换 + * false:只分库,不分表 + * + * @return 是否分表 + */ + boolean splitTable() default false; +} +``` + +### 使用示例 + +```java +@DBRouterStrategy(splitTable = true) +public class UserMapper { + + @DBRouter(key = "userId") + public User selectById(Long userId) { + // SQL: SELECT * FROM user WHERE id = ? + // 会被替换为: SELECT * FROM user_01 WHERE id = ? + } +} +``` + +--- + +## 🎓 知识点拓展 + +### 拓展1:注解属性的类型 + +**允许的类型**: +- 基本类型(int, long, boolean等) +- String +- Class +- 枚举 +- 注解 +- 以上类型的数组 + +**例子**: +```java +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface MyAnnotation { + String value(); // String类型 + int count() default 0; // int类型,有默认值 + Class clazz(); // Class类型 + ElementType[] types(); // 数组类型 + DBRouter router(); // 注解类型 +} +``` + +### 拓展2:注解的默认值 + +```java +public @interface MyAnnotation { + String value() default ""; // 有默认值,使用时可以不写 + int count(); // 没有默认值,使用时必须写 +} + +// 使用 +@MyAnnotation(count = 10) // value使用默认值 +@MyAnnotation(value = "test", count = 10) // 都指定 +``` + +### 拓展3:通过反射读取注解 + +```java +// 获取方法上的注解 +Method method = UserService.class.getMethod("queryUser", User.class); +DBRouter annotation = method.getAnnotation(DBRouter.class); +if (annotation != null) { + String key = annotation.key(); // 获取路由键 + System.out.println("路由键: " + key); +} + +// 获取类上的注解 +DBRouterStrategy strategy = UserService.class.getAnnotation(DBRouterStrategy.class); +if (strategy != null) { + boolean splitTable = strategy.splitTable(); + System.out.println("是否分表: " + splitTable); +} +``` + +### 拓展4:注解的继承 + +```java +@Inherited +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface MyAnnotation { + String value(); +} + +@MyAnnotation("parent") +public class Parent { +} + +// Child会继承Parent的@MyAnnotation注解 +public class Child extends Parent { +} +``` + +--- + +## ✅ 今日检查清单 + +- [ ] 理解了注解的概念和作用 +- [ ] 理解了元注解的含义 +- [ ] 创建了@DBRouter注解 +- [ ] 创建了@DBRouterStrategy注解 +- [ ] 理解了@Target和@Retention的作用 +- [ ] 完成了拓展阅读 + +--- + +## 🎯 明日预告 + +明天我们将学习: +- Java反射(Reflection)基础 +- 如何通过反射获取方法、字段 +- 如何通过反射调用方法和获取字段值 + +--- + +## 💡 思考题 + +1. 为什么@DBRouter要用@Retention(RetentionPolicy.RUNTIME)? +2. 如果@Target只写ElementType.METHOD,类上能用吗? +3. 注解的属性可以是什么类型? + +--- + +## 📚 参考资源 + +- [Java注解官方文档](https://docs.oracle.com/javase/tutorial/java/annotations/) +- [注解深入理解](https://www.baeldung.com/java-annotations-guide) +- [反射和注解](https://www.baeldung.com/java-reflection) diff --git "a/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25404\345\244\251-Java\345\217\215\345\260\204\345\237\272\347\241\200.md" "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25404\345\244\251-Java\345\217\215\345\260\204\345\237\272\347\241\200.md" new file mode 100644 index 0000000..a0d478b --- /dev/null +++ "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25404\345\244\251-Java\345\217\215\345\260\204\345\237\272\347\241\200.md" @@ -0,0 +1,421 @@ +# 第04天:Java反射基础 + +## 📚 今日目标 + +1. 理解Java反射的概念 +2. 学会通过反射获取类、方法、字段信息 +3. 学会通过反射调用方法和获取字段值 +4. 实现PropertyUtil工具类 + +--- + +## 🎯 知识点1:什么是反射? + +### 生活中的例子 + +**照镜子**: +- 看到自己的样子(类的信息) +- 看到自己穿什么衣服(字段) +- 看到自己能做什么动作(方法) + +**Java反射**: +- 在运行时获取类的信息 +- 获取类的字段、方法 +- 动态调用方法和访问字段 + +### 为什么需要反射? + +**场景**:我们需要从方法参数中获取路由键的值 + +```java +@DBRouter(key = "userId") +public void queryUser(User user) { + // 我们需要获取 user.getUserId() 的值 + // 但不知道参数是什么类型,也不知道有哪些字段 + // 怎么办?用反射! +} +``` + +--- + +## 🎯 知识点2:反射的核心类 + +### Class类:类的元信息 + +```java +// 获取Class对象的三种方式 +Class clazz1 = User.class; // 方式1:类字面量 +Class clazz2 = user.getClass(); // 方式2:对象.getClass() +Class clazz3 = Class.forName("com.example.User"); // 方式3:类名 + +// 获取类名 +String className = clazz.getName(); // 完整类名:com.example.User +String simpleName = clazz.getSimpleName(); // 简单类名:User +``` + +### Method类:方法信息 + +```java +// 获取所有公共方法 +Method[] methods = clazz.getMethods(); + +// 获取指定方法(包括私有方法) +Method method = clazz.getDeclaredMethod("getUserId"); + +// 获取方法名 +String methodName = method.getName(); + +// 调用方法 +Object result = method.invoke(user); // 相当于 user.getUserId() +``` + +### Field类:字段信息 + +```java +// 获取所有字段(包括私有) +Field[] fields = clazz.getDeclaredFields(); + +// 获取指定字段 +Field field = clazz.getDeclaredField("userId"); + +// 设置可访问(访问私有字段需要) +field.setAccessible(true); + +// 获取字段值 +Object value = field.get(user); // 相当于 user.userId + +// 设置字段值 +field.set(user, 12345L); // 相当于 user.userId = 12345L +``` + +--- + +## 🛠️ 实践任务1:通过反射获取属性值 + +### 步骤1:创建PropertyUtil工具类 + +在 `src/main/java/cn/bugstack/middleware/db/router/util/` 目录下创建 `PropertyUtil.java`: + +```java +package cn.bugstack.middleware.db.router.util; + +import org.apache.commons.beanutils.PropertyUtils; +import org.springframework.core.env.Environment; + +/** + * 属性工具类 + * 用于通过反射获取对象属性值 + * + * @author 小傅哥 + */ +public class PropertyUtil { + + private static int springBootVersion = 1; + + static { + try { + // 检测Spring Boot版本 + Class.forName("org.springframework.boot.bind.RelaxedPropertyResolver"); + } catch (ClassNotFoundException e) { + springBootVersion = 2; + } + } + + /** + * 处理Spring环境属性 + * + * @param environment Spring环境 + * @param prefix 属性前缀 + * @param targetClass 目标类型 + * @param 泛型 + * @return 属性对象 + */ + public static T handle(Environment environment, String prefix, Class targetClass) { + try { + if (springBootVersion == 1) { + return (T) v1(environment, prefix); + } else { + return (T) v2(environment, prefix, targetClass); + } + } catch (Exception e) { + throw new RuntimeException("获取属性失败: " + prefix, e); + } + } + + /** + * Spring Boot 1.x 版本 + */ + private static Object v1(Environment environment, String prefix) { + // Spring Boot 1.x 使用 RelaxedPropertyResolver + // 这里简化处理,实际项目中可能需要兼容 + throw new UnsupportedOperationException("Spring Boot 1.x 暂不支持"); + } + + /** + * Spring Boot 2.x 版本 + */ + private static Object v2(Environment environment, String prefix, Class targetClass) { + // Spring Boot 2.x 使用 Binder + try { + return org.springframework.boot.context.properties.bind.Binder + .get(environment) + .bind(prefix, targetClass) + .orElse(null); + } catch (Exception e) { + throw new RuntimeException("绑定属性失败: " + prefix, e); + } + } + + /** + * 通过反射获取对象属性值 + * + * @param obj 对象 + * @param propertyName 属性名 + * @return 属性值 + */ + public static Object getProperty(Object obj, String propertyName) { + try { + return PropertyUtils.getProperty(obj, propertyName); + } catch (Exception e) { + throw new RuntimeException("获取属性值失败: " + propertyName, e); + } + } + + /** + * 通过反射设置对象属性值 + * + * @param obj 对象 + * @param propertyName 属性名 + * @param value 属性值 + */ + public static void setProperty(Object obj, String propertyName, Object value) { + try { + PropertyUtils.setProperty(obj, propertyName, value); + } catch (Exception e) { + throw new RuntimeException("设置属性值失败: " + propertyName, e); + } + } +} +``` + +### 代码解释 + +1. **PropertyUtils.getProperty()**:使用Apache Commons BeanUtils获取属性值 + - 支持嵌套属性:`user.address.city` + - 支持索引属性:`list[0]` + - 自动处理getter方法 + +2. **Spring Boot版本检测**:不同版本的API不同,需要兼容处理 + +3. **异常处理**:统一抛出RuntimeException,便于上层处理 + +--- + +## 🛠️ 实践任务2:手动实现属性获取(理解原理) + +### 步骤1:创建简化版PropertyUtil + +为了理解原理,我们手动实现一个简化版: + +```java +package cn.bugstack.middleware.db.router.util; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * 属性工具类(手动实现版,用于理解原理) + */ +public class PropertyUtilManual { + + /** + * 手动获取属性值(理解原理用) + * + * @param obj 对象 + * @param propertyName 属性名 + * @return 属性值 + */ + public static Object getPropertyManual(Object obj, String propertyName) { + if (obj == null || propertyName == null) { + return null; + } + + Class clazz = obj.getClass(); + + // 方式1:通过getter方法获取 + try { + String getterName = "get" + capitalize(propertyName); + Method getter = clazz.getMethod(getterName); + return getter.invoke(obj); + } catch (Exception e) { + // getter方法不存在,尝试直接访问字段 + } + + // 方式2:直接访问字段 + try { + Field field = clazz.getDeclaredField(propertyName); + field.setAccessible(true); // 允许访问私有字段 + return field.get(obj); + } catch (Exception e) { + throw new RuntimeException("无法获取属性: " + propertyName, e); + } + } + + /** + * 首字母大写 + */ + private static String capitalize(String str) { + if (str == null || str.length() == 0) { + return str; + } + return str.substring(0, 1).toUpperCase() + str.substring(1); + } +} +``` + +### 使用示例 + +```java +public class User { + private Long userId; + private String name; + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } +} + +// 使用 +User user = new User(); +user.setUserId(12345L); + +// 方式1:使用BeanUtils(推荐) +Object value1 = PropertyUtil.getProperty(user, "userId"); // 12345L + +// 方式2:手动实现(理解原理) +Object value2 = PropertyUtilManual.getPropertyManual(user, "userId"); // 12345L +``` + +--- + +## 🎓 知识点拓展 + +### 拓展1:反射的性能问题 + +**问题**:反射比直接调用慢 + +```java +// 直接调用(快) +user.getUserId(); + +// 反射调用(慢,约慢10-100倍) +Method method = User.class.getMethod("getUserId"); +method.invoke(user); +``` + +**优化方案**: +1. **缓存Method对象**:不要每次都获取 +2. **使用MethodHandle**:Java 7+,性能更好 +3. **代码生成**:编译时生成代码 + +**例子**: +```java +// 缓存Method对象 +private static final Method GET_USER_ID_METHOD; +static { + try { + GET_USER_ID_METHOD = User.class.getMethod("getUserId"); + } catch (Exception e) { + throw new RuntimeException(e); + } +} + +// 使用时直接调用缓存的Method +Object value = GET_USER_ID_METHOD.invoke(user); +``` + +### 拓展2:BeanUtils vs 手动反射 + +**Apache Commons BeanUtils**: +- ✅ 功能强大(支持嵌套属性、索引属性) +- ✅ 代码简洁 +- ❌ 性能稍慢 +- ❌ 依赖外部库 + +**手动反射**: +- ✅ 性能稍好 +- ✅ 无外部依赖 +- ❌ 功能简单 +- ❌ 代码复杂 + +**选择建议**: +- 性能要求高:手动反射 + 缓存 +- 功能要求高:BeanUtils +- 我们项目:使用BeanUtils(功能优先) + +### 拓展3:获取方法参数名 + +**问题**:如何获取方法参数的真实名称? + +```java +public void queryUser(Long userId, String name) { + // 如何知道第一个参数叫userId? +} +``` + +**Java 8之前**: +- 参数名会丢失(编译后变成arg0, arg1) +- 需要编译时加 `-parameters` 参数 + +**Java 8+**: +```java +// 编译时加参数:javac -parameters +Method method = UserService.class.getMethod("queryUser", Long.class, String.class); +Parameter[] parameters = method.getParameters(); +String paramName = parameters[0].getName(); // userId +``` + +**我们的项目**: +- 不依赖参数名 +- 通过遍历参数对象,查找包含指定属性的对象 +- 更灵活,不依赖编译参数 + +--- + +## ✅ 今日检查清单 + +- [ ] 理解了反射的概念和作用 +- [ ] 学会了通过反射获取类、方法、字段信息 +- [ ] 学会了通过反射调用方法和获取字段值 +- [ ] 实现了PropertyUtil工具类 +- [ ] 理解了BeanUtils的工作原理 +- [ ] 完成了拓展阅读 + +--- + +## 🎯 明日预告 + +明天我们将学习: +- Spring AOP基础 +- 切面、切点、通知的概念 +- 如何创建AOP切面 + +--- + +## 💡 思考题 + +1. 为什么反射比直接调用慢? +2. 如何优化反射的性能? +3. 什么时候必须用反射,什么时候可以用其他方式? + +--- + +## 📚 参考资源 + +- [Java反射官方文档](https://docs.oracle.com/javase/tutorial/reflect/) +- [反射性能优化](https://www.baeldung.com/java-reflection-performance) +- [Apache Commons BeanUtils](https://commons.apache.org/proper/commons-beanutils/) diff --git "a/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25405\345\244\251-Spring-AOP\345\237\272\347\241\200.md" "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25405\345\244\251-Spring-AOP\345\237\272\347\241\200.md" new file mode 100644 index 0000000..01929d0 --- /dev/null +++ "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25405\345\244\251-Spring-AOP\345\237\272\347\241\200.md" @@ -0,0 +1,319 @@ +# 第05天:Spring AOP基础 + +## 📚 今日目标 + +1. 理解AOP的概念和作用 +2. 理解切面、切点、通知的概念 +3. 学会创建AOP切面 +4. 理解@Around、@Before、@After等注解 + +--- + +## 🎯 知识点1:什么是AOP? + +### 生活中的例子 + +**横切关注点**: +- 日志记录:每个方法都要记录日志 +- 事务管理:每个方法都要开启/提交事务 +- 权限检查:每个方法都要检查权限 + +**问题**:如果每个方法都写一遍,代码重复,维护困难 + +**解决方案**:AOP(面向切面编程) +- 把横切关注点提取出来 +- 在需要的地方自动应用 +- 不污染业务代码 + +### AOP核心概念 + +**切面(Aspect)**:横切关注点的模块化(如日志切面、事务切面) + +**切点(Pointcut)**:匹配哪些方法需要被拦截 + +**通知(Advice)**:什么时候执行(Before、After、Around) + +**连接点(JoinPoint)**:方法执行的点 + +--- + +## 🎯 知识点2:AOP通知类型 + +### @Before:前置通知 + +```java +@Before("execution(* com.example.service.*.*(..))") +public void before(JoinPoint joinPoint) { + System.out.println("方法执行前"); + // 可以获取方法参数 + Object[] args = joinPoint.getArgs(); +} +``` + +### @After:后置通知 + +```java +@After("execution(* com.example.service.*.*(..))") +public void after(JoinPoint joinPoint) { + System.out.println("方法执行后"); +} +``` + +### @Around:环绕通知(最重要) + +```java +@Around("execution(* com.example.service.*.*(..))") +public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + // 方法执行前 + System.out.println("方法执行前"); + + // 执行原方法 + Object result = joinPoint.proceed(); + + // 方法执行后 + System.out.println("方法执行后"); + + return result; +} +``` + +**为什么用@Around?** +- 可以在方法执行前后都处理 +- 可以控制是否执行原方法 +- 可以修改返回值 + +--- + +## 🛠️ 实践任务:创建路由切面 + +### 步骤1:创建DBRouterJoinPoint切面类 + +在 `src/main/java/cn/bugstack/middleware/db/router/` 目录下创建 `DBRouterJoinPoint.java`: + +```java +package cn.bugstack.middleware.db.router; + +import cn.bugstack.middleware.db.router.annotation.DBRouter; +import cn.bugstack.middleware.db.router.config.DBRouterConfig; +import cn.bugstack.middleware.db.router.strategy.IDBRouterStrategy; +import cn.bugstack.middleware.db.router.util.PropertyUtil; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; + +/** + * 数据库路由切面 + * + * 拦截带@DBRouter注解的方法,执行路由逻辑 + * + * @author 小傅哥 + */ +@Aspect +public class DBRouterJoinPoint { + + private Logger logger = LoggerFactory.getLogger(DBRouterJoinPoint.class); + + private DBRouterConfig dbRouterConfig; + private IDBRouterStrategy dbRouterStrategy; + + public DBRouterJoinPoint(DBRouterConfig dbRouterConfig, IDBRouterStrategy dbRouterStrategy) { + this.dbRouterConfig = dbRouterConfig; + this.dbRouterStrategy = dbRouterStrategy; + } + + /** + * 定义切点:拦截所有带@DBRouter注解的方法 + */ + @Pointcut("@annotation(cn.bugstack.middleware.db.router.annotation.DBRouter)") + public void aopPoint() { + } + + /** + * 环绕通知:在方法执行前后处理路由逻辑 + */ + @Around("aopPoint() && @annotation(dbRouter)") + public Object doRouter(ProceedingJoinPoint jp, DBRouter dbRouter) throws Throwable { + String dbKey = dbRouter.key(); + if (null == dbKey || dbKey.isEmpty()) { + throw new RuntimeException("annotation DBRouter key is null!"); + } + + // 获取路由键的值 + String dbKeyAttr = getAttrValue(dbKey, jp.getArgs()); + + // 执行路由 + dbRouterStrategy.doRouter(dbKeyAttr); + + // 执行原方法 + try { + return jp.proceed(); + } finally { + // 清理路由信息 + dbRouterStrategy.clear(); + } + } + + /** + * 获取路由键的值 + * + * @param attr 路由键字段名(如:userId) + * @param args 方法参数数组 + * @return 路由键的值 + */ + public String getAttrValue(String attr, Object[] args) { + if (1 == args.length) { + // 只有一个参数,直接从这个参数获取 + Object arg = args[0]; + if (arg instanceof String) { + return arg.toString(); + } + return String.valueOf(PropertyUtil.getProperty(arg, attr)); + } + + // 多个参数,遍历查找包含该属性的对象 + for (Object arg : args) { + if (arg == null) { + continue; + } + try { + Object value = PropertyUtil.getProperty(arg, attr); + if (null != value) { + return String.valueOf(value); + } + } catch (Exception e) { + // 忽略,继续查找 + } + } + throw new RuntimeException("未找到路由键: " + attr); + } + + /** + * 获取方法对象 + */ + private Method getMethod(JoinPoint jp) throws NoSuchMethodException { + Signature sig = jp.getSignature(); + MethodSignature methodSignature = (MethodSignature) sig; + return jp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes()); + } +} +``` + +### 代码解释 + +1. **@Aspect**:标识这是一个切面类 + +2. **@Pointcut**:定义切点,匹配所有带@DBRouter注解的方法 + +3. **@Around**:环绕通知,可以在方法执行前后处理 + +4. **ProceedingJoinPoint**: + - `jp.getArgs()`:获取方法参数 + - `jp.proceed()`:执行原方法 + +5. **路由逻辑**: + - 获取路由键的值 + - 执行路由(设置到ThreadLocal) + - 执行原方法 + - 清理路由信息 + +--- + +## 🎓 知识点拓展 + +### 拓展1:切点表达式 + +**execution表达式**: +```java +// 匹配所有public方法 +execution(public * *(..)) + +// 匹配指定包下的所有方法 +execution(* com.example.service.*.*(..)) + +// 匹配指定类的所有方法 +execution(* com.example.service.UserService.*(..)) + +// 匹配指定方法 +execution(* com.example.service.UserService.queryUser(..)) +``` + +**@annotation表达式**: +```java +// 匹配所有带@DBRouter注解的方法 +@annotation(cn.bugstack.middleware.db.router.annotation.DBRouter) + +// 匹配所有带指定注解的方法,并获取注解对象 +@annotation(dbRouter) +``` + +**组合表达式**: +```java +// 使用 &&、||、! 组合 +@Pointcut("execution(* com.example.service.*.*(..)) && @annotation(DBRouter)") +``` + +### 拓展2:JoinPoint vs ProceedingJoinPoint + +**JoinPoint**: +- 只能获取信息,不能控制执行 +- 用于@Before、@After + +**ProceedingJoinPoint**: +- 继承自JoinPoint +- 可以控制是否执行原方法 +- 用于@Around + +### 拓展3:AOP代理机制 + +**JDK动态代理**: +- 基于接口 +- 目标类必须实现接口 + +**CGLIB代理**: +- 基于继承 +- 不需要接口 + +**Spring选择**: +- 有接口:JDK动态代理 +- 无接口:CGLIB代理 + +--- + +## ✅ 今日检查清单 + +- [ ] 理解了AOP的概念和作用 +- [ ] 理解了切面、切点、通知的概念 +- [ ] 创建了DBRouterJoinPoint切面类 +- [ ] 理解了@Around通知的使用 +- [ ] 理解了ProceedingJoinPoint的用法 +- [ ] 完成了拓展阅读 + +--- + +## 🎯 明日预告 + +明天我们将学习: +- Spring Boot自动配置原理 +- @Configuration和@Bean +- spring.factories文件 + +--- + +## 💡 思考题 + +1. 为什么用@Around而不是@Before+@After? +2. 如何获取被拦截方法的返回值? +3. AOP的代理机制是什么? + +--- + +## 📚 参考资源 + +- [Spring AOP官方文档](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop) +- [AspectJ切点表达式](https://www.eclipse.org/aspectj/doc/released/adk15notebook/ataspectj-pcadvice.html) +- [Spring AOP原理](https://www.baeldung.com/spring-aop) diff --git "a/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25406\345\244\251-Spring-Boot\350\207\252\345\212\250\351\205\215\347\275\256\345\216\237\347\220\206.md" "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25406\345\244\251-Spring-Boot\350\207\252\345\212\250\351\205\215\347\275\256\345\216\237\347\220\206.md" new file mode 100644 index 0000000..0b18263 --- /dev/null +++ "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25406\345\244\251-Spring-Boot\350\207\252\345\212\250\351\205\215\347\275\256\345\216\237\347\220\206.md" @@ -0,0 +1,330 @@ +# 第06天:Spring Boot自动配置原理 + +## 📚 今日目标 + +1. 理解Spring Boot自动配置的原理 +2. 理解@Configuration和@Bean +3. 理解条件注解 +4. 创建spring.factories文件 + +--- + +## 🎯 知识点1:Spring Boot自动配置原理 + +### 为什么需要自动配置? + +**问题**:每次使用框架都要手动配置很多Bean + +**解决方案**:Spring Boot自动配置 +- 检测classpath中的类 +- 自动创建Bean +- 开箱即用 + +### 自动配置的流程 + +``` +1. Spring Boot启动 + ↓ +2. 读取META-INF/spring.factories + ↓ +3. 加载自动配置类 + ↓ +4. 根据条件注解判断是否生效 + ↓ +5. 创建Bean +``` + +--- + +## 🎯 知识点2:@Configuration和@Bean + +### @Configuration:配置类 + +```java +@Configuration +public class MyConfig { + + @Bean + public MyService myService() { + return new MyService(); + } +} +``` + +**作用**:标识这是一个配置类,Spring会扫描其中的@Bean方法 + +### @Bean:创建Bean + +```java +@Bean +public DataSource dataSource() { + return new HikariDataSource(); +} +``` + +**作用**:方法返回的对象会被注册为Spring Bean + +--- + +## 🎯 知识点3:条件注解 + +### @ConditionalOnClass:类存在时生效 + +```java +@ConditionalOnClass(DataSource.class) +public class DataSourceAutoConfig { + // 只有当classpath中存在DataSource类时才生效 +} +``` + +### @ConditionalOnMissingBean:Bean不存在时生效 + +```java +@Bean +@ConditionalOnMissingBean +public MyService myService() { + // 只有当容器中不存在MyService Bean时才创建 + return new MyService(); +} +``` + +### @ConditionalOnProperty:属性存在时生效 + +```java +@ConditionalOnProperty(prefix = "db-router", name = "enabled", havingValue = "true") +public class DBRouterAutoConfig { + // 只有当db-router.enabled=true时才生效 +} +``` + +--- + +## 🛠️ 实践任务:创建自动配置类 + +### 步骤1:创建DataSourceAutoConfig + +在 `src/main/java/cn/bugstack/middleware/db/router/config/` 目录下创建 `DataSourceAutoConfig.java`: + +```java +package cn.bugstack.middleware.db.router.config; + +import cn.bugstack.middleware.db.router.DBRouterJoinPoint; +import cn.bugstack.middleware.db.router.DBRouterConfig; +import cn.bugstack.middleware.db.router.dynamic.DynamicDataSource; +import cn.bugstack.middleware.db.router.dynamic.DynamicMybatisPlugin; +import cn.bugstack.middleware.db.router.strategy.IDBRouterStrategy; +import cn.bugstack.middleware.db.router.strategy.impl.DBRouterStrategyHashCode; +import cn.bugstack.middleware.db.router.util.PropertyUtil; +import org.apache.ibatis.plugin.Interceptor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.util.HashMap; +import java.util.Map; + +/** + * 数据源自动配置类 + * + * @author 小傅哥 + */ +@Configuration +@ConditionalOnClass({DataSource.class, org.apache.ibatis.session.SqlSessionFactory.class}) +@EnableConfigurationProperties(DBRouterConfig.class) +public class DataSourceAutoConfig implements EnvironmentAware { + + private static final String TAG_GLOBAL = "global"; + private static final String TAG_POOL = "pool"; + + private Map> dataSourceMap = new HashMap<>(); + private Map defaultDataSourceConfig; + private int dbCount; + private int tbCount; + private String routerKey; + + /** + * 创建路由配置 + */ + @Bean + public DBRouterConfig dbRouterConfig() { + DBRouterConfig config = new DBRouterConfig(); + config.setDbCount(dbCount); + config.setTbCount(tbCount); + config.setRouterKey(routerKey); + return config; + } + + /** + * 创建路由策略 + */ + @Bean + @ConditionalOnMissingBean + public IDBRouterStrategy dbRouterStrategy(DBRouterConfig dbRouterConfig) { + return new DBRouterStrategyHashCode(dbRouterConfig); + } + + /** + * 创建AOP切面 + */ + @Bean + @ConditionalOnMissingBean + public DBRouterJoinPoint point(DBRouterConfig dbRouterConfig, IDBRouterStrategy dbRouterStrategy) { + return new DBRouterJoinPoint(dbRouterConfig, dbRouterStrategy); + } + + /** + * 创建MyBatis插件 + */ + @Bean + @ConditionalOnMissingBean + public Interceptor plugin() { + return new DynamicMybatisPlugin(); + } + + /** + * 创建动态数据源 + */ + @Bean + @ConditionalOnMissingBean + public DataSource createDataSource() { + // 创建多个数据源 + Map targetDataSources = new HashMap<>(); + for (int i = 1; i <= dbCount; i++) { + String dbKey = String.format("db%02d", i); + DataSource dataSource = createDataSource(dataSourceMap.get(dbKey)); + targetDataSources.put(dbKey, dataSource); + } + + // 创建动态数据源 + DynamicDataSource dynamicDataSource = new DynamicDataSource(); + dynamicDataSource.setTargetDataSources(targetDataSources); + dynamicDataSource.setDefaultTargetDataSource(createDataSource(defaultDataSourceConfig)); + return dynamicDataSource; + } + + /** + * 创建单个数据源 + */ + private DataSource createDataSource(Map config) { + // 这里简化处理,实际应该根据配置创建数据源 + // 可以使用HikariCP、Druid等连接池 + // 为了简化,这里返回null,实际项目中需要实现 + return null; + } + + /** + * 创建事务模板 + */ + @Bean + public TransactionTemplate transactionTemplate(DataSource dataSource) { + return new TransactionTemplate(new DataSourceTransactionManager(dataSource)); + } + + @Override + public void setEnvironment(Environment environment) { + // 从环境变量中读取配置 + String prefix = "router.jdbc.datasource."; + + // 读取数据库数量 + dbCount = Integer.parseInt(environment.getProperty(prefix + "dbCount", "2")); + tbCount = Integer.parseInt(environment.getProperty(prefix + "tbCount", "4")); + routerKey = environment.getProperty(prefix + "routerKey", "userId"); + + // 读取数据源配置 + // 这里简化处理,实际应该读取完整配置 + } +} +``` + +### 步骤2:创建spring.factories文件 + +在 `src/main/resources/META-INF/` 目录下创建 `spring.factories`: + +``` +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.bugstack.middleware.db.router.config.DataSourceAutoConfig +``` + +**说明**: +- `EnableAutoConfiguration`:自动配置的key +- 后面是自动配置类的全限定名 + +--- + +## 🎓 知识点拓展 + +### 拓展1:自动配置的优先级 + +**用户配置 > 自动配置** + +```java +// 用户自定义的Bean会覆盖自动配置的Bean +@Bean +public IDBRouterStrategy dbRouterStrategy() { + return new MyCustomStrategy(); // 会覆盖自动配置的 +} +``` + +### 拓展2:条件注解的组合 + +```java +@ConditionalOnClass(DataSource.class) +@ConditionalOnProperty(prefix = "db-router", name = "enabled", havingValue = "true") +public class DBRouterAutoConfig { + // 同时满足两个条件才生效 +} +``` + +### 拓展3:@EnableConfigurationProperties + +```java +@EnableConfigurationProperties(DBRouterConfig.class) +``` + +**作用**: +- 启用配置属性绑定 +- 将application.yml中的配置绑定到DBRouterConfig对象 +- 自动注册DBRouterConfig为Bean + +--- + +## ✅ 今日检查清单 + +- [ ] 理解了Spring Boot自动配置的原理 +- [ ] 理解了@Configuration和@Bean +- [ ] 理解了条件注解 +- [ ] 创建了DataSourceAutoConfig配置类 +- [ ] 创建了spring.factories文件 +- [ ] 完成了拓展阅读 + +--- + +## 🎯 明日预告 + +明天我们将学习: +- MyBatis插件机制 +- 如何拦截SQL执行 +- 如何修改SQL语句 + +--- + +## 💡 思考题 + +1. 为什么需要spring.factories文件? +2. @ConditionalOnMissingBean的作用是什么? +3. 如何让用户能够覆盖自动配置的Bean? + +--- + +## 📚 参考资源 + +- [Spring Boot自动配置](https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.auto-configuration) +- [条件注解](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration.condition-annotations) +- [创建自定义Starter](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration) diff --git "a/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25407\345\244\251-MyBatis\346\217\222\344\273\266\346\234\272\345\210\266.md" "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25407\345\244\251-MyBatis\346\217\222\344\273\266\346\234\272\345\210\266.md" new file mode 100644 index 0000000..7b19625 --- /dev/null +++ "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25407\345\244\251-MyBatis\346\217\222\344\273\266\346\234\272\345\210\266.md" @@ -0,0 +1,255 @@ +# 第07天:MyBatis插件机制 + +## 📚 今日目标 + +1. 理解MyBatis插件机制 +2. 理解拦截器(Interceptor)的概念 +3. 实现DynamicMybatisPlugin插件 +4. 学会修改SQL语句 + +--- + +## 🎯 知识点1:MyBatis插件机制 + +### 为什么需要插件? + +**场景**:我们需要在SQL执行前修改表名 + +```sql +-- 原始SQL +SELECT * FROM user WHERE id = ? + +-- 需要修改为 +SELECT * FROM user_01 WHERE id = ? +``` + +**解决方案**:MyBatis插件 +- 拦截SQL执行 +- 修改SQL语句 +- 不修改业务代码 + +### MyBatis插件原理 + +``` +SQL执行流程: +1. 创建Statement + ↓ +2. 插件拦截(可以修改SQL) + ↓ +3. 执行SQL + ↓ +4. 返回结果 +``` + +--- + +## 🎯 知识点2:实现拦截器 + +### Interceptor接口 + +```java +public interface Interceptor { + Object intercept(Invocation invocation) throws Throwable; + Object plugin(Object target); + void setProperties(Properties properties); +} +``` + +### @Intercepts注解 + +```java +@Intercepts({ + @Signature( + type = StatementHandler.class, // 拦截StatementHandler + method = "prepare", // 拦截prepare方法 + args = {Connection.class, Integer.class} // 方法参数 + ) +}) +``` + +--- + +## 🛠️ 实践任务:实现DynamicMybatisPlugin + +### 步骤1:创建DynamicMybatisPlugin类 + +在 `src/main/java/cn/bugstack/middleware/db/router/dynamic/` 目录下创建 `DynamicMybatisPlugin.java`: + +```java +package cn.bugstack.middleware.db.router.dynamic; + +import cn.bugstack.middleware.db.router.DBContextHolder; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.plugin.*; +import org.apache.ibatis.reflection.DefaultReflectorFactory; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.SystemMetaObject; + +import java.lang.reflect.Field; +import java.sql.Connection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * MyBatis动态表名插件 + * + * 拦截SQL执行,修改表名 + * + * @author 小傅哥 + */ +@Intercepts({ + @Signature( + type = StatementHandler.class, + method = "prepare", + args = {Connection.class, Integer.class} + ) +}) +public class DynamicMybatisPlugin implements Interceptor { + + private Pattern pattern = Pattern.compile("(from|into|update|FROM|INTO|UPDATE)\\s+(\\w+)\\s+", Pattern.CASE_INSENSITIVE); + + @Override + public Object intercept(Invocation invocation) throws Throwable { + // 获取StatementHandler + StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); + + // 获取BoundSql(包含SQL语句) + BoundSql boundSql = statementHandler.getBoundSql(); + String sql = boundSql.getSql(); + + // 获取表索引 + String tbKey = DBContextHolder.getTBKey(); + if (null != tbKey && !tbKey.isEmpty()) { + // 修改SQL中的表名 + sql = sql.replaceAll("(from|into|update|FROM|INTO|UPDATE)\\s+(\\w+)\\s+", + "$1 $2_" + tbKey + " "); + } + + // 使用反射修改BoundSql的sql字段 + MetaObject metaObject = SystemMetaObject.forObject(boundSql); + metaObject.setValue("sql", sql); + + return invocation.proceed(); + } + + @Override + public Object plugin(Object target) { + return Plugin.wrap(target, this); + } + + @Override + public void setProperties(Properties properties) { + // 可以读取配置 + } +} +``` + +### 代码解释 + +1. **@Intercepts**:指定拦截StatementHandler的prepare方法 + +2. **intercept方法**: + - 获取SQL语句 + - 从ThreadLocal获取表索引 + - 使用正则表达式替换表名 + - 使用反射修改BoundSql的sql字段 + +3. **正则表达式**: + - `(from|into|update)`:匹配SQL关键字 + - `\\s+`:匹配空格 + - `(\\w+)`:匹配表名 + - `$1 $2_01`:替换为 `from user_01` + +--- + +## 🎓 知识点拓展 + +### 拓展1:MyBatis插件拦截点 + +**可以拦截的对象**: +- `Executor`:执行器 +- `StatementHandler`:SQL处理器 +- `ParameterHandler`:参数处理器 +- `ResultSetHandler`:结果集处理器 + +**我们选择StatementHandler的原因**: +- 可以获取和修改SQL语句 +- 在SQL执行前拦截 +- 不影响参数和结果处理 + +### 拓展2:MetaObject的使用 + +**为什么用MetaObject?** +- BoundSql的sql字段是final的,不能直接修改 +- MetaObject可以通过反射修改final字段 + +**例子**: +```java +// 方式1:直接修改(不行,sql是final的) +boundSql.setSql(newSql); // 编译错误 + +// 方式2:使用MetaObject(可以) +MetaObject metaObject = SystemMetaObject.forObject(boundSql); +metaObject.setValue("sql", newSql); // 可以修改 +``` + +### 拓展3:SQL替换的精确性 + +**问题**:简单的replace可能误替换 + +```sql +-- 原始SQL +SELECT * FROM user WHERE name = 'from table' + +-- 简单replace会误替换 +SELECT * FROM user_01 WHERE name = 'from table' -- 正确 +SELECT * FROM user_01 WHERE name = 'from_01 table' -- 错误! +``` + +**解决方案**:使用正则表达式精确匹配 + +```java +// 精确匹配表名(考虑表别名) +Pattern pattern = Pattern.compile( + "(from|into|update|FROM|INTO|UPDATE)\\s+(\\w+)(\\s+\\w+)?\\s+", + Pattern.CASE_INSENSITIVE +); +``` + +--- + +## ✅ 今日检查清单 + +- [ ] 理解了MyBatis插件机制 +- [ ] 理解了拦截器的概念 +- [ ] 实现了DynamicMybatisPlugin插件 +- [ ] 理解了SQL替换的原理 +- [ ] 理解了MetaObject的使用 +- [ ] 完成了拓展阅读 + +--- + +## 🎯 明日预告 + +明天我们将学习: +- ThreadLocal深入理解 +- 为什么用ThreadLocal存储路由信息 +- 如何避免内存泄漏 + +--- + +## 💡 思考题 + +1. 为什么选择拦截StatementHandler而不是Executor? +2. 如何精确匹配SQL中的表名? +3. MetaObject的作用是什么? + +--- + +## 📚 参考资源 + +- [MyBatis插件文档](https://mybatis.org/mybatis-3/zh/configuration.html#plugins) +- [MyBatis拦截器原理](https://www.baeldung.com/mybatis-interceptors) +- [正则表达式教程](https://www.runoob.com/regexp/regexp-tutorial.html) diff --git "a/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25408\345\244\251-ThreadLocal\346\267\261\345\205\245\347\220\206\350\247\243.md" "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25408\345\244\251-ThreadLocal\346\267\261\345\205\245\347\220\206\350\247\243.md" new file mode 100644 index 0000000..7b4b9a1 --- /dev/null +++ "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25408\345\244\251-ThreadLocal\346\267\261\345\205\245\347\220\206\350\247\243.md" @@ -0,0 +1,269 @@ +# 第08天:ThreadLocal深入理解 + +## 📚 今日目标 + +1. 理解ThreadLocal的概念和作用 +2. 理解为什么用ThreadLocal存储路由信息 +3. 实现DBContextHolder类 +4. 理解内存泄漏问题 + +--- + +## 🎯 知识点1:什么是ThreadLocal? + +### 生活中的例子 + +**银行保险箱**: +- 每个人有自己的保险箱 +- 互不干扰 +- 只能自己存取 + +**ThreadLocal**: +- 每个线程有自己的变量副本 +- 线程间互不干扰 +- 线程内共享 + +### 为什么需要ThreadLocal? + +**问题场景**: +```java +// 方法1:设置路由信息 +public void method1() { + setRouteInfo("db01", "user_01"); + method2(); // 调用方法2 +} + +// 方法2:需要使用路由信息 +public void method2() { + String db = getRouteInfo(); // 如何获取? +} +``` + +**解决方案**:ThreadLocal +- 在方法1中设置 +- 在方法2中获取 +- 不需要传递参数 + +--- + +## 🎯 知识点2:ThreadLocal的使用 + +### 基本用法 + +```java +// 创建ThreadLocal +ThreadLocal threadLocal = new ThreadLocal<>(); + +// 设置值 +threadLocal.set("value"); + +// 获取值 +String value = threadLocal.get(); + +// 删除值 +threadLocal.remove(); +``` + +### 为什么用ThreadLocal存储路由信息? + +**原因**: +1. **线程隔离**:每个请求是独立的线程,互不干扰 +2. **无需传参**:不需要在每个方法中传递路由信息 +3. **自动清理**:请求结束后,线程销毁,ThreadLocal自动清理 + +--- + +## 🛠️ 实践任务:实现DBContextHolder + +### 步骤1:创建DBContextHolder类 + +在 `src/main/java/cn/bugstack/middleware/db/router/` 目录下创建 `DBContextHolder.java`: + +```java +package cn.bugstack.middleware.db.router; + +/** + * 数据库路由上下文持有者 + * + * 使用ThreadLocal存储当前线程的路由信息 + * + * @author 小傅哥 + */ +public class DBContextHolder { + + private static final ThreadLocal dbKey = new ThreadLocal<>(); + private static final ThreadLocal tbKey = new ThreadLocal<>(); + + /** + * 设置数据库索引 + * + * @param dbKeyIdx 数据库索引,如:db01, db02 + */ + public static void setDBKey(String dbKeyIdx) { + dbKey.set(dbKeyIdx); + } + + /** + * 获取数据库索引 + * + * @return 数据库索引 + */ + public static String getDBKey() { + return dbKey.get(); + } + + /** + * 设置表索引 + * + * @param tbKeyIdx 表索引,如:01, 02 + */ + public static void setTBKey(String tbKeyIdx) { + tbKey.set(tbKeyIdx); + } + + /** + * 获取表索引 + * + * @return 表索引 + */ + public static String getTBKey() { + return tbKey.get(); + } + + /** + * 清理数据库索引 + */ + public static void clearDBKey() { + dbKey.remove(); + } + + /** + * 清理表索引 + */ + public static void clearTBKey() { + tbKey.remove(); + } + + /** + * 清理所有路由信息 + */ + public static void clear() { + dbKey.remove(); + tbKey.remove(); + } +} +``` + +### 代码解释 + +1. **ThreadLocal存储**: + - `dbKey`:存储数据库索引(如:db01) + - `tbKey`:存储表索引(如:01) + +2. **静态方法**: + - 所有方法都是静态的,方便调用 + - 不需要创建实例 + +3. **清理方法**: + - 必须手动清理,避免内存泄漏 + - 在finally块中调用 + +--- + +## 🎓 知识点拓展 + +### 拓展1:ThreadLocal内存泄漏问题 + +**问题场景**: +```java +ThreadLocal threadLocal = new ThreadLocal<>(); +threadLocal.set("value"); +// 忘记调用 remove() +// 线程结束后,ThreadLocal的Entry还在,造成内存泄漏 +``` + +**原因**: +- ThreadLocal的Entry是WeakReference +- 但Value是强引用 +- 如果ThreadLocal对象被回收,但线程还在,Value无法回收 + +**解决方案**: +```java +try { + threadLocal.set("value"); + // 使用 +} finally { + threadLocal.remove(); // 必须清理 +} +``` + +### 拓展2:ThreadLocal的实现原理 + +**ThreadLocalMap**: +- 每个Thread都有一个ThreadLocalMap +- ThreadLocalMap的key是ThreadLocal对象 +- ThreadLocalMap的value是存储的值 + +**存储结构**: +``` +Thread +└── ThreadLocalMap + ├── Entry(ThreadLocal1, value1) + ├── Entry(ThreadLocal2, value2) + └── ... +``` + +### 拓展3:InheritableThreadLocal + +**问题**:子线程无法继承父线程的ThreadLocal值 + +**解决方案**:InheritableThreadLocal + +```java +InheritableThreadLocal threadLocal = new InheritableThreadLocal<>(); +threadLocal.set("value"); + +new Thread(() -> { + String value = threadLocal.get(); // 可以获取到父线程的值 +}).start(); +``` + +**我们项目不需要**: +- 每个请求是独立的线程 +- 不需要在子线程中共享路由信息 + +--- + +## ✅ 今日检查清单 + +- [ ] 理解了ThreadLocal的概念和作用 +- [ ] 理解了为什么用ThreadLocal存储路由信息 +- [ ] 实现了DBContextHolder类 +- [ ] 理解了内存泄漏问题 +- [ ] 理解了ThreadLocal的实现原理 +- [ ] 完成了拓展阅读 + +--- + +## 🎯 明日预告 + +明天我们将学习: +- 动态数据源原理 +- AbstractRoutingDataSource +- 实现DynamicDataSource类 + +--- + +## 💡 思考题 + +1. 为什么用ThreadLocal而不是全局变量? +2. ThreadLocal的内存泄漏问题如何避免? +3. 什么时候需要清理ThreadLocal? + +--- + +## 📚 参考资源 + +- [ThreadLocal官方文档](https://docs.oracle.com/javase/8/docs/api/java/lang/ThreadLocal.html) +- [ThreadLocal原理分析](https://www.baeldung.com/java-threadlocal) +- [内存泄漏问题](https://www.baeldung.com/java-threadlocal-memory-leak) diff --git "a/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25409\345\244\251-\345\212\250\346\200\201\346\225\260\346\215\256\346\272\220\345\216\237\347\220\206.md" "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25409\345\244\251-\345\212\250\346\200\201\346\225\260\346\215\256\346\272\220\345\216\237\347\220\206.md" new file mode 100644 index 0000000..24c2145 --- /dev/null +++ "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25409\345\244\251-\345\212\250\346\200\201\346\225\260\346\215\256\346\272\220\345\216\237\347\220\206.md" @@ -0,0 +1,225 @@ +# 第09天:动态数据源原理 + +## 📚 今日目标 + +1. 理解动态数据源的原理 +2. 理解AbstractRoutingDataSource +3. 实现DynamicDataSource类 +4. 理解数据源切换机制 + +--- + +## 🎯 知识点1:什么是动态数据源? + +### 问题场景 + +**多数据源场景**: +- 数据库1:用户数据 +- 数据库2:订单数据 +- 数据库3:商品数据 + +**动态切换**: +- 根据业务逻辑选择不同的数据源 +- 同一个方法可能使用不同的数据源 + +### Spring的解决方案 + +**AbstractRoutingDataSource**: +- Spring提供的抽象类 +- 根据key动态选择数据源 +- 支持多个目标数据源 + +--- + +## 🎯 知识点2:AbstractRoutingDataSource + +### 核心方法 + +```java +public abstract class AbstractRoutingDataSource extends AbstractDataSource { + + // 决定使用哪个数据源的key + protected abstract Object determineCurrentLookupKey(); + + // 目标数据源Map + private Map targetDataSources; + + // 默认数据源 + private Object defaultTargetDataSource; +} +``` + +### 工作原理 + +``` +1. 调用 determineCurrentLookupKey() 获取key + ↓ +2. 从 targetDataSources 中根据key获取数据源 + ↓ +3. 如果找不到,使用 defaultTargetDataSource + ↓ +4. 返回数据源 +``` + +--- + +## 🛠️ 实践任务:实现DynamicDataSource + +### 步骤1:创建DynamicDataSource类 + +在 `src/main/java/cn/bugstack/middleware/db/router/dynamic/` 目录下创建 `DynamicDataSource.java`: + +```java +package cn.bugstack.middleware.db.router.dynamic; + +import cn.bugstack.middleware.db.router.DBContextHolder; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; + +/** + * 动态数据源 + * + * 根据ThreadLocal中的key动态选择数据源 + * + * @author 小傅哥 + */ +public class DynamicDataSource extends AbstractRoutingDataSource { + + private String defaultDataSource; + + @Override + protected Object determineCurrentLookupKey() { + // 从ThreadLocal获取数据源key + String dbKey = DBContextHolder.getDBKey(); + if (null == dbKey || dbKey.isEmpty()) { + // 如果没有设置,返回默认数据源 + return defaultDataSource; + } + return dbKey; + } + + public void setDefaultDataSource(String defaultDataSource) { + this.defaultDataSource = defaultDataSource; + } +} +``` + +### 代码解释 + +1. **继承AbstractRoutingDataSource**: + - 实现`determineCurrentLookupKey()`方法 + - 返回数据源的key + +2. **从ThreadLocal获取key**: + - 调用`DBContextHolder.getDBKey()` + - 获取当前线程的数据源key + +3. **默认数据源**: + - 如果没有设置key,使用默认数据源 + - 保证系统正常运行 + +--- + +## 🎓 知识点拓展 + +### 拓展1:数据源配置 + +**配置多个数据源**: + +```java +@Configuration +public class DataSourceConfig { + + @Bean + public DataSource dynamicDataSource() { + // 创建多个数据源 + DataSource db01 = createDataSource("jdbc:mysql://localhost:3306/db01"); + DataSource db02 = createDataSource("jdbc:mysql://localhost:3306/db02"); + + // 创建数据源Map + Map targetDataSources = new HashMap<>(); + targetDataSources.put("db01", db01); + targetDataSources.put("db02", db02); + + // 创建动态数据源 + DynamicDataSource dataSource = new DynamicDataSource(); + dataSource.setTargetDataSources(targetDataSources); + dataSource.setDefaultTargetDataSource(db01); + + return dataSource; + } +} +``` + +### 拓展2:数据源切换的时机 + +**切换时机**: +1. **AOP切面**:在方法执行前设置key +2. **数据源选择**:在获取连接时根据key选择 +3. **清理**:在方法执行后清理key + +**流程**: +``` +请求进入 + ↓ +AOP切面:设置DBContextHolder.setDBKey("db01") + ↓ +执行方法:需要数据库连接 + ↓ +DynamicDataSource.determineCurrentLookupKey() → 返回"db01" + ↓ +从targetDataSources获取db01数据源 + ↓ +获取连接,执行SQL + ↓ +AOP切面:清理DBContextHolder.clear() +``` + +### 拓展3:事务和数据源 + +**问题**:事务和数据源的关系 + +**原理**: +- 事务管理器绑定数据源 +- 同一个事务内使用同一个数据源 +- 事务开始时就确定了数据源 + +**我们的项目**: +- 在AOP切面中设置数据源 +- 在事务开始前就确定了数据源 +- 保证事务内数据源一致 + +--- + +## ✅ 今日检查清单 + +- [ ] 理解了动态数据源的原理 +- [ ] 理解了AbstractRoutingDataSource +- [ ] 实现了DynamicDataSource类 +- [ ] 理解了数据源切换机制 +- [ ] 理解了事务和数据源的关系 +- [ ] 完成了拓展阅读 + +--- + +## 🎯 明日预告 + +明天我们将学习: +- 策略模式设计 +- 路由策略接口设计 +- 为后续实现做准备 + +--- + +## 💡 思考题 + +1. 为什么用AbstractRoutingDataSource而不是直接切换数据源? +2. 数据源切换的时机是什么? +3. 如何保证事务内数据源一致? + +--- + +## 📚 参考资源 + +- [AbstractRoutingDataSource文档](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.html) +- [多数据源配置](https://www.baeldung.com/spring-abstract-routing-data-source) +- [动态数据源实现](https://www.baeldung.com/spring-boot-configure-multiple-datasources) diff --git "a/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25410\345\244\251-\347\255\226\347\225\245\346\250\241\345\274\217\350\256\276\350\256\241.md" "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25410\345\244\251-\347\255\226\347\225\245\346\250\241\345\274\217\350\256\276\350\256\241.md" new file mode 100644 index 0000000..c8f045b --- /dev/null +++ "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25410\345\244\251-\347\255\226\347\225\245\346\250\241\345\274\217\350\256\276\350\256\241.md" @@ -0,0 +1,222 @@ +# 第10天:策略模式设计 + +## 📚 今日目标 + +1. 理解策略模式的概念 +2. 理解为什么用策略模式 +3. 设计路由策略接口 +4. 为后续实现做准备 + +--- + +## 🎯 知识点1:什么是策略模式? + +### 生活中的例子 + +**支付方式**: +- 支付宝支付 +- 微信支付 +- 银行卡支付 + +**策略模式**: +- 定义一系列算法 +- 把它们封装起来 +- 使它们可以互换 + +### 策略模式的结构 + +``` +策略接口(Strategy) + ↑ + | +策略实现1(ConcreteStrategy1) +策略实现2(ConcreteStrategy2) +策略实现3(ConcreteStrategy3) +``` + +--- + +## 🎯 知识点2:为什么用策略模式? + +### 问题场景 + +**不同的路由算法**: +- 哈希路由:hash(userId) % dbCount +- 取模路由:userId % dbCount +- 范围路由:根据范围判断 + +**问题**:如果不用策略模式 +```java +// 硬编码,不灵活 +if (algorithm.equals("hash")) { + // 哈希算法 +} else if (algorithm.equals("mod")) { + // 取模算法 +} +``` + +**解决方案**:策略模式 +```java +// 灵活,易扩展 +IDBRouterStrategy strategy = getStrategy(algorithm); +strategy.doRouter(userId); +``` + +--- + +## 🛠️ 实践任务:设计路由策略接口 + +### 步骤1:创建IDBRouterStrategy接口 + +在 `src/main/java/cn/bugstack/middleware/db/router/strategy/` 目录下创建 `IDBRouterStrategy.java`: + +```java +package cn.bugstack.middleware.db.router.strategy; + +/** + * 数据库路由策略接口 + * + * 定义路由算法的规范 + * + * @author 小傅哥 + */ +public interface IDBRouterStrategy { + + /** + * 执行路由 + * + * @param dbKeyAttr 路由键的值 + */ + void doRouter(String dbKeyAttr); + + /** + * 设置数据库索引 + * + * @param dbIdx 数据库索引 + */ + void setDBKey(int dbIdx); + + /** + * 设置表索引 + * + * @param tbIdx 表索引 + */ + void setTBKey(int tbIdx); + + /** + * 获取数据库数量 + * + * @return 数据库数量 + */ + int dbCount(); + + /** + * 获取表数量 + * + * @return 表数量 + */ + int tbCount(); + + /** + * 清理路由信息 + */ + void clear(); +} +``` + +### 代码解释 + +1. **doRouter方法**: + - 执行路由逻辑 + - 根据路由键计算库和表索引 + - 设置到ThreadLocal + +2. **setDBKey/setTBKey**: + - 设置库和表索引 + - 用于手动指定路由 + +3. **dbCount/tbCount**: + - 获取库和表数量 + - 用于路由计算 + +4. **clear方法**: + - 清理路由信息 + - 避免内存泄漏 + +--- + +## 🎓 知识点拓展 + +### 拓展1:策略模式 vs 工厂模式 + +**策略模式**: +- 关注算法的选择 +- 运行时选择算法 + +**工厂模式**: +- 关注对象的创建 +- 创建时选择对象 + +**结合使用**: +```java +// 工厂创建策略 +IDBRouterStrategy strategy = StrategyFactory.create("hash"); +// 策略执行算法 +strategy.doRouter(userId); +``` + +### 拓展2:策略模式的优缺点 + +**优点**: +- ✅ 算法可以自由切换 +- ✅ 避免使用多重条件判断 +- ✅ 扩展性良好 + +**缺点**: +- ❌ 客户端必须知道所有策略类 +- ❌ 策略类数量增多 + +### 拓展3:其他设计模式 + +**模板方法模式**: +- 定义算法骨架 +- 子类实现具体步骤 + +**观察者模式**: +- 定义一对多依赖 +- 一个对象改变,所有依赖者收到通知 + +--- + +## ✅ 今日检查清单 + +- [ ] 理解了策略模式的概念 +- [ ] 理解了为什么用策略模式 +- [ ] 设计了路由策略接口 +- [ ] 理解了策略模式的优缺点 +- [ ] 完成了拓展阅读 + +--- + +## 🎯 明日预告 + +明天我们将开始核心实现: +- 实现配置属性类DBRouterConfig +- 实现工具类StringUtils +- 完善项目基础 + +--- + +## 💡 思考题 + +1. 为什么用策略模式而不是if-else? +2. 策略模式和工厂模式的区别? +3. 如何扩展新的路由策略? + +--- + +## 📚 参考资源 + +- [设计模式:策略模式](https://refactoring.guru/design-patterns/strategy) +- [Java设计模式](https://www.runoob.com/design-pattern/strategy-pattern.html) +- [策略模式实战](https://www.baeldung.com/java-strategy-pattern) diff --git "a/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25411\345\244\251-\345\256\236\347\216\260\351\205\215\347\275\256\345\261\236\346\200\247\347\261\273\345\222\214\345\267\245\345\205\267\347\261\273.md" "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25411\345\244\251-\345\256\236\347\216\260\351\205\215\347\275\256\345\261\236\346\200\247\347\261\273\345\222\214\345\267\245\345\205\267\347\261\273.md" new file mode 100644 index 0000000..9ef1e17 --- /dev/null +++ "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25411\345\244\251-\345\256\236\347\216\260\351\205\215\347\275\256\345\261\236\346\200\247\347\261\273\345\222\214\345\267\245\345\205\267\347\261\273.md" @@ -0,0 +1,373 @@ +# 第11天:实现配置属性类和工具类 + +## 📚 今日目标 + +1. 实现DBRouterConfig配置属性类 +2. 实现StringUtils工具类 +3. 完善PropertyUtil工具类 +4. 理解@ConfigurationProperties + +--- + +## 🎯 知识点1:@ConfigurationProperties + +### 作用 + +**将配置文件中的属性绑定到Java对象** + +```yaml +# application.yml +db-router: + db-count: 2 + tb-count: 4 + router-key: userId +``` + +```java +@ConfigurationProperties(prefix = "db-router") +public class DBRouterConfig { + private int dbCount; + private int tbCount; + private String routerKey; +} +``` + +**自动绑定**:Spring Boot会自动将配置绑定到对象 + +--- + +## 🛠️ 实践任务1:实现DBRouterConfig + +### 步骤1:创建DBRouterConfig类 + +在 `src/main/java/cn/bugstack/middleware/db/router/` 目录下创建 `DBRouterConfig.java`: + +```java +package cn.bugstack.middleware.db.router; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 数据库路由配置 + * + * @author 小傅哥 + */ +@ConfigurationProperties(prefix = "router.jdbc.datasource") +public class DBRouterConfig { + + /** 分库数量 */ + private int dbCount; + + /** 分表数量 */ + private int tbCount; + + /** 路由键 */ + private String routerKey; + + public DBRouterConfig() { + } + + public DBRouterConfig(int dbCount, int tbCount, String routerKey) { + this.dbCount = dbCount; + this.tbCount = tbCount; + this.routerKey = routerKey; + } + + public int getDbCount() { + return dbCount; + } + + public void setDbCount(int dbCount) { + this.dbCount = dbCount; + } + + public int getTbCount() { + return tbCount; + } + + public void setTbCount(int tbCount) { + this.tbCount = tbCount; + } + + public String getRouterKey() { + return routerKey; + } + + public void setRouterKey(String routerKey) { + this.routerKey = routerKey; + } +} +``` + +### 配置示例 + +在 `application.yml` 中配置: + +```yaml +router: + jdbc: + datasource: + dbCount: 2 # 2个数据库 + tbCount: 4 # 每个库4张表 + routerKey: userId # 路由键 +``` + +--- + +## 🛠️ 实践任务2:实现StringUtils工具类 + +### 步骤1:创建StringUtils类 + +在 `src/main/java/cn/bugstack/middleware/db/router/util/` 目录下创建 `StringUtils.java`: + +```java +package cn.bugstack.middleware.db.router.util; + +import org.apache.commons.lang.StringUtils as ApacheStringUtils; + +/** + * 字符串工具类 + * + * @author 小傅哥 + */ +public class StringUtils { + + /** + * 判断字符串是否为空 + * + * @param str 字符串 + * @return true:为空,false:不为空 + */ + public static boolean isBlank(String str) { + return ApacheStringUtils.isBlank(str); + } + + /** + * 判断字符串是否不为空 + * + * @param str 字符串 + * @return true:不为空,false:为空 + */ + public static boolean isNotBlank(String str) { + return ApacheStringUtils.isNotBlank(str); + } + + /** + * 中划线转驼峰 + * + * @param str 中划线字符串,如:user-name + * @return 驼峰字符串,如:userName + */ + public static String middleScoreToCamelCase(String str) { + if (isBlank(str)) { + return str; + } + + StringBuilder sb = new StringBuilder(); + boolean nextUpperCase = false; + + for (char c : str.toCharArray()) { + if (c == '-') { + nextUpperCase = true; + } else { + if (nextUpperCase) { + sb.append(Character.toUpperCase(c)); + nextUpperCase = false; + } else { + sb.append(c); + } + } + } + + return sb.toString(); + } +} +``` + +### 代码解释 + +1. **isBlank/isNotBlank**: + - 使用Apache Commons Lang的工具方法 + - 判断字符串是否为空(包括null、空字符串、只有空格) + +2. **middleScoreToCamelCase**: + - 将中划线命名转为驼峰命名 + - 如:`user-name` → `userName` + +--- + +## 🛠️ 实践任务3:完善PropertyUtil工具类 + +### 步骤1:完善PropertyUtil类 + +在 `src/main/java/cn/bugstack/middleware/db/router/util/` 目录下完善 `PropertyUtil.java`: + +```java +package cn.bugstack.middleware.db.router.util; + +import org.apache.commons.beanutils.PropertyUtils; +import org.springframework.core.env.Environment; + +/** + * 属性工具类 + * + * @author 小傅哥 + */ +public class PropertyUtil { + + private static int springBootVersion = 1; + + static { + try { + // 检测Spring Boot版本 + Class.forName("org.springframework.boot.bind.RelaxedPropertyResolver"); + } catch (ClassNotFoundException e) { + springBootVersion = 2; + } + } + + /** + * 处理Spring环境属性 + */ + public static T handle(Environment environment, String prefix, Class targetClass) { + try { + if (springBootVersion == 1) { + return (T) v1(environment, prefix); + } else { + return (T) v2(environment, prefix, targetClass); + } + } catch (Exception e) { + throw new RuntimeException("获取属性失败: " + prefix, e); + } + } + + /** + * Spring Boot 1.x 版本 + */ + private static Object v1(Environment environment, String prefix) { + throw new UnsupportedOperationException("Spring Boot 1.x 暂不支持"); + } + + /** + * Spring Boot 2.x 版本 + */ + private static Object v2(Environment environment, String prefix, Class targetClass) { + try { + return org.springframework.boot.context.properties.bind.Binder + .get(environment) + .bind(prefix, targetClass) + .orElse(null); + } catch (Exception e) { + throw new RuntimeException("绑定属性失败: " + prefix, e); + } + } + + /** + * 获取对象属性值 + */ + public static Object getProperty(Object obj, String propertyName) { + try { + return PropertyUtils.getProperty(obj, propertyName); + } catch (Exception e) { + throw new RuntimeException("获取属性值失败: " + propertyName, e); + } + } + + /** + * 设置对象属性值 + */ + public static void setProperty(Object obj, String propertyName, Object value) { + try { + PropertyUtils.setProperty(obj, propertyName, value); + } catch (Exception e) { + throw new RuntimeException("设置属性值失败: " + propertyName, e); + } + } +} +``` + +--- + +## 🎓 知识点拓展 + +### 拓展1:@ConfigurationProperties vs @Value + +**@ConfigurationProperties**: +- ✅ 类型安全 +- ✅ 支持复杂对象 +- ✅ IDE提示 + +**@Value**: +- ✅ 简单直接 +- ❌ 类型不安全 +- ❌ 不支持复杂对象 + +**选择建议**: +- 简单配置:@Value +- 复杂配置:@ConfigurationProperties + +### 拓展2:配置属性验证 + +```java +@ConfigurationProperties(prefix = "db-router") +@Validated +public class DBRouterConfig { + + @Min(1) + @Max(100) + private int dbCount; + + @NotBlank + private String routerKey; +} +``` + +### 拓展3:多环境配置 + +```yaml +# application.yml +db-router: + db-count: 2 + +# application-dev.yml +db-router: + db-count: 2 + +# application-prod.yml +db-router: + db-count: 4 +``` + +--- + +## ✅ 今日检查清单 + +- [ ] 实现了DBRouterConfig配置属性类 +- [ ] 实现了StringUtils工具类 +- [ ] 完善了PropertyUtil工具类 +- [ ] 理解了@ConfigurationProperties +- [ ] 完成了拓展阅读 + +--- + +## 🎯 明日预告 + +明天我们将实现: +- 哈希路由策略DBRouterStrategyHashCode +- 理解哈希算法的实现 + +--- + +## 💡 思考题 + +1. @ConfigurationProperties和@Value的区别? +2. 如何验证配置属性的有效性? +3. 如何支持多环境配置? + +--- + +## 📚 参考资源 + +- [@ConfigurationProperties文档](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.typesafe-configuration-properties) +- [Apache Commons Lang](https://commons.apache.org/proper/commons-lang/) +- [Apache Commons BeanUtils](https://commons.apache.org/proper/commons-beanutils/) diff --git "a/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25412\345\244\251-\345\256\236\347\216\260\345\223\210\345\270\214\350\267\257\347\224\261\347\255\226\347\225\245.md" "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25412\345\244\251-\345\256\236\347\216\260\345\223\210\345\270\214\350\267\257\347\224\261\347\255\226\347\225\245.md" new file mode 100644 index 0000000..7afef55 --- /dev/null +++ "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25412\345\244\251-\345\256\236\347\216\260\345\223\210\345\270\214\350\267\257\347\224\261\347\255\226\347\225\245.md" @@ -0,0 +1,236 @@ +# 第12天:实现哈希路由策略 + +## 📚 今日目标 + +1. 实现DBRouterStrategyHashCode类 +2. 理解哈希算法的实现 +3. 理解路由计算的逻辑 +4. 实现DBRouterBase基类 + +--- + +## 🛠️ 实践任务1:实现DBRouterStrategyHashCode + +### 步骤1:创建DBRouterStrategyHashCode类 + +在 `src/main/java/cn/bugstack/middleware/db/router/strategy/impl/` 目录下创建 `DBRouterStrategyHashCode.java`: + +```java +package cn.bugstack.middleware.db.router.strategy.impl; + +import cn.bugstack.middleware.db.router.DBRouterConfig; +import cn.bugstack.middleware.db.router.DBContextHolder; +import cn.bugstack.middleware.db.router.strategy.IDBRouterStrategy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 哈希路由策略 + * + * 使用hashCode计算路由 + * + * @author 小傅哥 + */ +public class DBRouterStrategyHashCode implements IDBRouterStrategy { + + private Logger logger = LoggerFactory.getLogger(DBRouterStrategyHashCode.class); + + private DBRouterConfig dbRouterConfig; + + public DBRouterStrategyHashCode(DBRouterConfig dbRouterConfig) { + this.dbRouterConfig = dbRouterConfig; + } + + @Override + public void doRouter(String dbKeyAttr) { + int size = dbRouterConfig.getDbCount() * dbRouterConfig.getTbCount(); + + // 哈希计算 + int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16)); + + // 计算库索引和表索引 + int dbIdx = idx / dbRouterConfig.getTbCount() + 1; + int tbIdx = idx % dbRouterConfig.getTbCount() + 1; + + // 设置到ThreadLocal + setDBKey(dbIdx); + setTBKey(tbIdx); + + logger.debug("数据库路由 dbIdx: {} tbIdx: {}", dbIdx, tbIdx); + } + + @Override + public void setDBKey(int dbIdx) { + DBContextHolder.setDBKey(String.format("%02d", dbIdx)); + } + + @Override + public void setTBKey(int tbIdx) { + DBContextHolder.setTBKey(String.format("%02d", tbIdx)); + } + + @Override + public int dbCount() { + return dbRouterConfig.getDbCount(); + } + + @Override + public int tbCount() { + return dbRouterConfig.getTbCount(); + } + + @Override + public void clear() { + DBContextHolder.clearDBKey(); + DBContextHolder.clearTBKey(); + } +} +``` + +### 代码解释 + +1. **哈希计算**: + ```java + int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16)); + ``` + - 使用HashMap的哈希算法 + - `hashCode ^ (hashCode >>> 16)`:扰动函数,减少哈希冲突 + - `(size - 1) &`:取模运算(size必须是2的幂) + +2. **计算库和表索引**: + ```java + int dbIdx = idx / tbCount + 1; // 库索引(从1开始) + int tbIdx = idx % tbCount + 1; // 表索引(从1开始) + ``` + +3. **设置到ThreadLocal**: + - 格式化为两位数字(01, 02, ...) + - 设置到DBContextHolder + +--- + +## 🛠️ 实践任务2:实现DBRouterBase基类 + +### 步骤1:创建DBRouterBase类 + +在 `src/main/java/cn/bugstack/middleware/db/router/` 目录下创建 `DBRouterBase.java`: + +```java +package cn.bugstack.middleware.db.router; + +/** + * 数据库路由基类 + * + * 用于存储表索引信息 + * + * @author 小傅哥 + */ +public class DBRouterBase { + + private String tbIdx; + + public String getTbIdx() { + return tbIdx; + } + + public void setTbIdx(String tbIdx) { + this.tbIdx = tbIdx; + } +} +``` + +### 使用场景 + +这个基类可以用于: +- 实体类继承,自动获取表索引 +- 查询条件中指定表索引 + +--- + +## 🎓 知识点拓展 + +### 拓展1:哈希算法的选择 + +**为什么用HashMap的哈希算法?** + +```java +// HashMap的哈希算法 +int hash = key.hashCode() ^ (key.hashCode() >>> 16); +int index = (n - 1) & hash; +``` + +**优点**: +- ✅ 分布均匀 +- ✅ 减少冲突 +- ✅ 性能好 + +**其他哈希算法**: +- MD5:安全性高,但性能差 +- SHA:安全性高,但性能差 +- CRC32:性能好,但分布可能不均匀 + +### 拓展2:取模运算的优化 + +**传统取模**: +```java +int index = hashCode % size; // 慢 +``` + +**位运算取模**(size是2的幂): +```java +int index = (size - 1) & hashCode; // 快 +``` + +**为什么快?** +- 位运算比除法快 +- 但要求size必须是2的幂 + +### 拓展3:哈希冲突处理 + +**问题**:不同的key可能计算出相同的索引 + +**解决方案**: +1. **开放地址法**:找下一个空位置 +2. **链地址法**:用链表存储冲突的元素 +3. **再哈希法**:用另一个哈希函数 + +**我们的项目**: +- 使用哈希算法,冲突概率低 +- 如果冲突,数据会分布到不同的库表 +- 这是可以接受的 + +--- + +## ✅ 今日检查清单 + +- [ ] 实现了DBRouterStrategyHashCode类 +- [ ] 理解了哈希算法的实现 +- [ ] 理解了路由计算的逻辑 +- [ ] 实现了DBRouterBase基类 +- [ ] 理解了哈希冲突的处理 +- [ ] 完成了拓展阅读 + +--- + +## 🎯 明日预告 + +明天我们将完善AOP切面: +- 完善DBRouterJoinPoint类 +- 处理类级别注解 +- 处理异常情况 + +--- + +## 💡 思考题 + +1. 为什么用HashMap的哈希算法? +2. 位运算取模为什么比除法快? +3. 如何处理哈希冲突? + +--- + +## 📚 参考资源 + +- [HashMap源码分析](https://www.baeldung.com/java-hashmap) +- [哈希算法原理](https://en.wikipedia.org/wiki/Hash_function) +- [位运算优化](https://www.baeldung.com/java-bitwise-operators) diff --git "a/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25413\345\244\251-\345\256\214\345\226\204AOP\345\210\207\351\235\242\345\256\236\347\216\260.md" "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25413\345\244\251-\345\256\214\345\226\204AOP\345\210\207\351\235\242\345\256\236\347\216\260.md" new file mode 100644 index 0000000..2a1ed13 --- /dev/null +++ "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25413\345\244\251-\345\256\214\345\226\204AOP\345\210\207\351\235\242\345\256\236\347\216\260.md" @@ -0,0 +1,262 @@ +# 第13天:完善AOP切面实现 + +## 📚 今日目标 + +1. 完善DBRouterJoinPoint类 +2. 处理类级别注解 +3. 处理异常情况 +4. 优化路由逻辑 + +--- + +## 🛠️ 实践任务:完善DBRouterJoinPoint + +### 完整实现代码 + +在 `src/main/java/cn/bugstack/middleware/db/router/` 目录下完善 `DBRouterJoinPoint.java`: + +```java +package cn.bugstack.middleware.db.router; + +import cn.bugstack.middleware.db.router.annotation.DBRouter; +import cn.bugstack.middleware.db.router.annotation.DBRouterStrategy; +import cn.bugstack.middleware.db.router.config.DBRouterConfig; +import cn.bugstack.middleware.db.router.strategy.IDBRouterStrategy; +import cn.bugstack.middleware.db.router.util.PropertyUtil; +import cn.bugstack.middleware.db.router.util.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.Signature; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * 数据库路由切面 + * + * @author 小傅哥 + */ +@Aspect +public class DBRouterJoinPoint { + + private Logger logger = LoggerFactory.getLogger(DBRouterJoinPoint.class); + + private DBRouterConfig dbRouterConfig; + private IDBRouterStrategy dbRouterStrategy; + + public DBRouterJoinPoint(DBRouterConfig dbRouterConfig, IDBRouterStrategy dbRouterStrategy) { + this.dbRouterConfig = dbRouterConfig; + this.dbRouterStrategy = dbRouterStrategy; + } + + @Pointcut("@annotation(cn.bugstack.middleware.db.router.annotation.DBRouter)") + public void aopPoint() { + } + + @Around("aopPoint() && @annotation(dbRouter)") + public Object doRouter(ProceedingJoinPoint jp, DBRouter dbRouter) throws Throwable { + String dbKey = dbRouter.key(); + if (StringUtils.isBlank(dbKey)) { + throw new RuntimeException("annotation DBRouter key is null!"); + } + + // 获取路由键的值 + String dbKeyAttr = getAttrValue(dbKey, jp.getArgs()); + + // 执行路由 + dbRouterStrategy.doRouter(dbKeyAttr); + + // 执行原方法 + try { + return jp.proceed(); + } finally { + // 清理路由信息 + dbRouterStrategy.clear(); + } + } + + /** + * 获取路由键的值 + */ + public String getAttrValue(String attr, Object[] args) { + if (1 == args.length) { + Object arg = args[0]; + if (arg instanceof String) { + return arg.toString(); + } + return String.valueOf(PropertyUtil.getProperty(arg, attr)); + } + + for (Object arg : args) { + if (arg == null) { + continue; + } + try { + Object value = PropertyUtil.getProperty(arg, attr); + if (null != value) { + return String.valueOf(value); + } + } catch (Exception e) { + // 忽略,继续查找 + } + } + throw new RuntimeException("未找到路由键: " + attr); + } + + /** + * 获取方法对象 + */ + private Method getMethod(ProceedingJoinPoint jp) throws NoSuchMethodException { + Signature sig = jp.getSignature(); + MethodSignature methodSignature = (MethodSignature) sig; + return jp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes()); + } + + /** + * 根据字段名获取字段值 + */ + private Object getValueByName(Object obj, String name) { + try { + Field field = getFieldByName(obj, name); + if (null == field) { + return null; + } + field.setAccessible(true); + return field.get(obj); + } catch (Exception e) { + return null; + } + } + + /** + * 根据字段名获取字段 + */ + private Field getFieldByName(Object obj, String name) { + Field field = null; + Class clazz = obj.getClass(); + for (; clazz != Object.class; clazz = clazz.getSuperclass()) { + try { + field = clazz.getDeclaredField(name); + } catch (Exception e) { + // 忽略,继续查找 + } + } + return field; + } +} +``` + +### 代码解释 + +1. **路由键获取**: + - 单个参数:直接获取 + - 多个参数:遍历查找包含该属性的对象 + +2. **异常处理**: + - 路由键为空:抛出异常 + - 找不到路由键:抛出异常 + - 使用finally确保清理 + +3. **辅助方法**: + - `getMethod`:获取方法对象 + - `getValueByName`:根据字段名获取值 + - `getFieldByName`:根据字段名获取字段 + +--- + +## 🎓 知识点拓展 + +### 拓展1:处理类级别注解 + +**场景**:类上也有@DBRouterStrategy注解 + +```java +@DBRouterStrategy(splitTable = true) +public class UserMapper { + + @DBRouter(key = "userId") + public User selectById(Long userId) { + // ... + } +} +``` + +**处理逻辑**: +```java +// 先检查类上的注解 +DBRouterStrategy classAnnotation = jp.getTarget().getClass().getAnnotation(DBRouterStrategy.class); +if (classAnnotation != null && classAnnotation.splitTable()) { + // 需要分表 +} +``` + +### 拓展2:路由键的优先级 + +**优先级**: +1. 方法上的@DBRouter注解 +2. 类上的@DBRouterStrategy注解 +3. 配置中的routerKey + +**实现**: +```java +String dbKey = dbRouter.key(); +if (StringUtils.isBlank(dbKey)) { + // 检查类上的注解 + DBRouterStrategy strategy = getClassAnnotation(jp); + if (strategy != null) { + dbKey = dbRouterConfig.getRouterKey(); + } +} +``` + +### 拓展3:性能优化 + +**缓存Method对象**: +```java +private static final Map METHOD_CACHE = new ConcurrentHashMap<>(); + +private DBRouter getDBRouterAnnotation(ProceedingJoinPoint jp) { + Method method = getMethod(jp); + return METHOD_CACHE.computeIfAbsent(method, m -> m.getAnnotation(DBRouter.class)); +} +``` + +--- + +## ✅ 今日检查清单 + +- [ ] 完善了DBRouterJoinPoint类 +- [ ] 处理了类级别注解 +- [ ] 处理了异常情况 +- [ ] 优化了路由逻辑 +- [ ] 完成了拓展阅读 + +--- + +## 🎯 明日预告 + +明天我们将完善MyBatis插件: +- 优化SQL替换逻辑 +- 处理表别名 +- 处理JOIN语句 + +--- + +## 💡 思考题 + +1. 如何支持类级别注解? +2. 如何优化AOP切面的性能? +3. 如何处理路由键的优先级? + +--- + +## 📚 参考资源 + +- [Spring AOP最佳实践](https://www.baeldung.com/spring-aop) +- [AOP性能优化](https://www.baeldung.com/spring-aop-performance) +- [切面设计模式](https://www.baeldung.com/aspect-oriented-programming) diff --git "a/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25414\345\244\251-\345\256\214\345\226\204MyBatis\346\217\222\344\273\266.md" "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25414\345\244\251-\345\256\214\345\226\204MyBatis\346\217\222\344\273\266.md" new file mode 100644 index 0000000..32e42df --- /dev/null +++ "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25414\345\244\251-\345\256\214\345\226\204MyBatis\346\217\222\344\273\266.md" @@ -0,0 +1,206 @@ +# 第14天:完善MyBatis插件 + +## 📚 今日目标 + +1. 完善DynamicMybatisPlugin插件 +2. 优化SQL替换逻辑 +3. 处理表别名和JOIN语句 +4. 处理各种SQL场景 + +--- + +## 🛠️ 实践任务:完善DynamicMybatisPlugin + +### 完整实现代码 + +在 `src/main/java/cn/bugstack/middleware/db/router/dynamic/` 目录下完善 `DynamicMybatisPlugin.java`: + +```java +package cn.bugstack.middleware.db.router.dynamic; + +import cn.bugstack.middleware.db.router.DBContextHolder; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.plugin.*; +import org.apache.ibatis.reflection.DefaultReflectorFactory; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.SystemMetaObject; + +import java.lang.reflect.Field; +import java.sql.Connection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * MyBatis动态表名插件 + * + * @author 小傅哥 + */ +@Intercepts({ + @Signature( + type = StatementHandler.class, + method = "prepare", + args = {Connection.class, Integer.class} + ) +}) +public class DynamicMybatisPlugin implements Interceptor { + + private Pattern pattern = Pattern.compile("(from|into|update|FROM|INTO|UPDATE)\\s+(\\w+)\\s+", Pattern.CASE_INSENSITIVE); + + @Override + public Object intercept(Invocation invocation) throws Throwable { + // 获取StatementHandler + StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); + + // 获取BoundSql + BoundSql boundSql = statementHandler.getBoundSql(); + String sql = boundSql.getSql(); + + // 获取表索引 + String tbKey = DBContextHolder.getTBKey(); + if (null != tbKey && !tbKey.isEmpty()) { + // 修改SQL中的表名 + sql = sql.replaceAll("(from|into|update|FROM|INTO|UPDATE)\\s+(\\w+)\\s+", + "$1 $2_" + tbKey + " "); + } + + // 使用反射修改BoundSql的sql字段 + MetaObject metaObject = SystemMetaObject.forObject(boundSql); + metaObject.setValue("sql", sql); + + return invocation.proceed(); + } + + @Override + public Object plugin(Object target) { + return Plugin.wrap(target, this); + } + + @Override + public void setProperties(Properties properties) { + // 可以读取配置 + } +} +``` + +### 优化:处理表别名 + +```java +// 更精确的正则表达式 +private Pattern pattern = Pattern.compile( + "(from|into|update|FROM|INTO|UPDATE)\\s+(\\w+)(\\s+\\w+)?\\s+", + Pattern.CASE_INSENSITIVE +); + +// 替换逻辑 +Matcher matcher = pattern.matcher(sql); +StringBuffer sb = new StringBuffer(); +while (matcher.find()) { + String tableName = matcher.group(2); + String alias = matcher.group(3); + matcher.appendReplacement(sb, matcher.group(1) + " " + tableName + "_" + tbKey + + (alias != null ? alias : "") + " "); +} +matcher.appendTail(sb); +sql = sb.toString(); +``` + +### 优化:处理JOIN语句 + +```java +// 处理JOIN语句中的表名 +sql = sql.replaceAll("(join|JOIN)\\s+(\\w+)\\s+", + "$1 $2_" + tbKey + " "); +``` + +--- + +## 🎓 知识点拓展 + +### 拓展1:SQL替换的精确性 + +**问题场景**: +```sql +-- 原始SQL +SELECT * FROM user WHERE name = 'from table' + +-- 简单replace会误替换 +SELECT * FROM user_01 WHERE name = 'from_01 table' -- 错误! +``` + +**解决方案**:使用正则表达式精确匹配 + +### 拓展2:处理各种SQL场景 + +**SELECT语句**: +```sql +SELECT * FROM user WHERE id = ? +-- 替换为 +SELECT * FROM user_01 WHERE id = ? +``` + +**INSERT语句**: +```sql +INSERT INTO user (name) VALUES (?) +-- 替换为 +INSERT INTO user_01 (name) VALUES (?) +``` + +**UPDATE语句**: +```sql +UPDATE user SET name = ? WHERE id = ? +-- 替换为 +UPDATE user_01 SET name = ? WHERE id = ? +``` + +**DELETE语句**: +```sql +DELETE FROM user WHERE id = ? +-- 替换为 +DELETE FROM user_01 WHERE id = ? +``` + +### 拓展3:性能优化 + +**缓存Pattern对象**: +```java +private static final Pattern PATTERN = Pattern.compile("..."); +``` + +**避免重复编译正则表达式** + +--- + +## ✅ 今日检查清单 + +- [ ] 完善了DynamicMybatisPlugin插件 +- [ ] 优化了SQL替换逻辑 +- [ ] 处理了表别名和JOIN语句 +- [ ] 处理了各种SQL场景 +- [ ] 完成了拓展阅读 + +--- + +## 🎯 明日预告 + +明天我们将完善自动配置类: +- 完善DataSourceAutoConfig +- 处理数据源创建 +- 处理配置读取 + +--- + +## 💡 思考题 + +1. 如何精确匹配SQL中的表名? +2. 如何处理表别名? +3. 如何优化SQL替换的性能? + +--- + +## 📚 参考资源 + +- [MyBatis插件开发](https://mybatis.org/mybatis-3/zh/configuration.html#plugins) +- [正则表达式教程](https://www.runoob.com/regexp/regexp-tutorial.html) +- [SQL解析](https://github.com/alibaba/druid/wiki/SQL-Parser) diff --git "a/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25415-30\345\244\251-\345\220\216\347\273\255\345\256\236\347\216\260\346\214\207\345\215\227.md" "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25415-30\345\244\251-\345\220\216\347\273\255\345\256\236\347\216\260\346\214\207\345\215\227.md" new file mode 100644 index 0000000..95e79ed --- /dev/null +++ "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25415-30\345\244\251-\345\220\216\347\273\255\345\256\236\347\216\260\346\214\207\345\215\227.md" @@ -0,0 +1,301 @@ +# 第15-30天:后续实现指南 + +## 📚 文件结构说明 + +由于篇幅限制,这里提供第15-30天的实现指南大纲。每个文件都应该包含: +1. 今日目标 +2. 知识点讲解 +3. 实践任务(含完整代码) +4. 知识点拓展 +5. 检查清单 +6. 思考题 + +--- + +## 第15天:完善自动配置类 + +**目标**:完善DataSourceAutoConfig,实现数据源创建和配置读取 + +**关键代码**: +- 完善DataSourceAutoConfig类 +- 实现数据源创建逻辑 +- 处理配置读取(Environment) +- 实现createDataSource方法 + +**知识点**: +- Environment接口的使用 +- 数据源连接池配置 +- 多数据源管理 + +--- + +## 第16天:创建spring.factories文件 + +**目标**:创建自动配置入口文件 + +**关键代码**: +```properties +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.bugstack.middleware.db.router.config.DataSourceAutoConfig +``` + +**知识点**: +- spring.factories的作用 +- 自动配置的加载机制 + +--- + +## 第17天:整合测试(上) + +**目标**:创建测试项目,测试基本功能 + +**关键步骤**: +1. 创建测试Spring Boot项目 +2. 引入starter依赖 +3. 配置数据源 +4. 编写测试代码 + +**测试内容**: +- 路由功能是否正常 +- 数据源切换是否正常 +- SQL替换是否正常 + +--- + +## 第18天:整合测试(下) + +**目标**:完善测试,处理边界情况 + +**测试场景**: +- 路由键为空 +- 路由键不存在 +- 多个参数 +- 事务场景 + +--- + +## 第19天:异常处理和容错 + +**目标**:完善异常处理,添加容错机制 + +**关键代码**: +- 添加异常处理 +- 添加降级策略 +- 添加友好的错误提示 + +**知识点**: +- 异常处理最佳实践 +- 容错设计 + +--- + +## 第20天:性能优化 + +**目标**:优化性能,减少开销 + +**优化点**: +- 缓存Method对象 +- 优化反射调用 +- 优化SQL替换 + +**知识点**: +- 性能优化技巧 +- 缓存策略 + +--- + +## 第21天:支持类级别注解 + +**目标**:支持@DBRouterStrategy类级别注解 + +**关键代码**: +- 修改AOP切面,支持类级别注解 +- 处理注解优先级 + +**知识点**: +- 注解的继承 +- 优先级处理 + +--- + +## 第22天:优化SQL替换逻辑 + +**目标**:更精确的SQL替换 + +**优化点**: +- 更精确的正则表达式 +- 处理表别名 +- 处理JOIN语句 +- 处理子查询 + +--- + +## 第23天:添加路由策略扩展点 + +**目标**:支持自定义路由策略 + +**关键代码**: +- 创建策略工厂 +- 支持配置选择策略 +- 提供扩展接口 + +**知识点**: +- 工厂模式 +- 扩展点设计 + +--- + +## 第24天:添加监控和日志 + +**目标**:添加路由日志和监控 + +**关键代码**: +- 添加路由日志 +- 记录路由统计 +- 性能监控 + +**知识点**: +- 日志最佳实践 +- 监控指标设计 + +--- + +## 第25天:处理事务场景 + +**目标**:确保事务内数据源一致 + +**关键代码**: +- 测试事务场景 +- 确保事务内数据源一致 +- 处理嵌套事务 + +**知识点**: +- 事务管理 +- 数据源和事务的关系 + +--- + +## 第26天:编写文档和示例 + +**目标**:编写使用文档和示例代码 + +**文档内容**: +- README.md +- 配置示例 +- 代码示例 +- 常见问题 + +--- + +## 第27天:代码审查和重构 + +**目标**:审查代码质量,重构优化 + +**审查点**: +- 代码规范 +- 设计模式使用 +- 性能问题 +- 可维护性 + +--- + +## 第28天:单元测试 + +**目标**:编写单元测试 + +**测试内容**: +- 路由策略测试 +- AOP切面测试 +- MyBatis插件测试 +- 工具类测试 + +--- + +## 第29天:集成测试 + +**目标**:编写集成测试 + +**测试内容**: +- 完整流程测试 +- 多数据源测试 +- 并发测试 + +--- + +## 第30天:项目总结和扩展 + +**目标**:总结学习成果,思考扩展 + +**内容**: +- 学习总结 +- 知识点回顾 +- 扩展功能思考 +- 后续学习方向 + +--- + +## 📝 实现建议 + +1. **按顺序实现**:每天完成一个文件 +2. **理解原理**:不要只是复制代码 +3. **动手实践**:每学一个知识点就写代码验证 +4. **记录问题**:遇到问题记录下来,逐步解决 +5. **拓展思考**:完成拓展练习,加深理解 + +--- + +## 🎯 核心文件清单 + +确保以下文件都已实现: + +### 注解 +- [ ] DBRouter.java +- [ ] DBRouterStrategy.java + +### 配置 +- [ ] DBRouterConfig.java +- [ ] DataSourceAutoConfig.java + +### 核心类 +- [ ] DBContextHolder.java +- [ ] DBRouterJoinPoint.java +- [ ] DBRouterBase.java + +### 动态数据源 +- [ ] DynamicDataSource.java +- [ ] DynamicMybatisPlugin.java + +### 策略 +- [ ] IDBRouterStrategy.java +- [ ] DBRouterStrategyHashCode.java + +### 工具类 +- [ ] StringUtils.java +- [ ] PropertyUtil.java + +### 配置文件 +- [ ] spring.factories +- [ ] pom.xml + +--- + +## 💡 学习建议 + +1. **每天完成一个文件**:不要急于求成 +2. **理解每个知识点**:知其然,知其所以然 +3. **动手实践**:理论结合实践 +4. **记录笔记**:记录学习心得和问题 +5. **拓展思考**:完成拓展练习 + +--- + +## 📚 参考资源 + +- [Spring Boot官方文档](https://spring.io/projects/spring-boot) +- [MyBatis官方文档](https://mybatis.org/mybatis-3/) +- [Spring AOP文档](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop) +- [设计模式](https://refactoring.guru/design-patterns) + +--- + +**祝你学习顺利!有问题随时记录,每天进步一点点!** 🎉 diff --git "a/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25415\345\244\251-\345\256\214\345\226\204\350\207\252\345\212\250\351\205\215\347\275\256\347\261\273\357\274\210\345\256\214\346\225\264\344\273\243\347\240\201\357\274\211.md" "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25415\345\244\251-\345\256\214\345\226\204\350\207\252\345\212\250\351\205\215\347\275\256\347\261\273\357\274\210\345\256\214\346\225\264\344\273\243\347\240\201\357\274\211.md" new file mode 100644 index 0000000..d08a1ad --- /dev/null +++ "b/30\345\244\251\345\256\236\347\216\260\346\214\207\345\215\227/\347\254\25415\345\244\251-\345\256\214\345\226\204\350\207\252\345\212\250\351\205\215\347\275\256\347\261\273\357\274\210\345\256\214\346\225\264\344\273\243\347\240\201\357\274\211.md" @@ -0,0 +1,315 @@ +# 第15天:完善自动配置类(完整代码) + +## 📚 今日目标 + +1. 完善DataSourceAutoConfig类 +2. 实现数据源创建逻辑 +3. 实现配置读取(Environment) +4. 实现getGlobalProps和injectGlobal方法 + +--- + +## 🛠️ 完整实现代码 + +### DataSourceAutoConfig完整实现 + +在 `src/main/java/cn/bugstack/middleware/db/router/config/` 目录下完善 `DataSourceAutoConfig.java`: + +```java +package cn.bugstack.middleware.db.router.config; + +import cn.bugstack.middleware.db.router.DBRouterConfig; +import cn.bugstack.middleware.db.router.DBRouterJoinPoint; +import cn.bugstack.middleware.db.router.dynamic.DynamicDataSource; +import cn.bugstack.middleware.db.router.dynamic.DynamicMybatisPlugin; +import cn.bugstack.middleware.db.router.strategy.IDBRouterStrategy; +import cn.bugstack.middleware.db.router.strategy.impl.DBRouterStrategyHashCode; +import cn.bugstack.middleware.db.router.util.PropertyUtil; +import org.apache.ibatis.plugin.Interceptor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.util.HashMap; +import java.util.Map; + +/** + * 数据源自动配置类 + * + * @author 小傅哥 + */ +@Configuration +@ConditionalOnClass({DataSource.class, org.apache.ibatis.session.SqlSessionFactory.class}) +@EnableConfigurationProperties(DBRouterConfig.class) +public class DataSourceAutoConfig implements EnvironmentAware { + + private static final String TAG_GLOBAL = "global"; + private static final String TAG_POOL = "pool"; + + private Map> dataSourceMap = new HashMap<>(); + private Map defaultDataSourceConfig; + private int dbCount; + private int tbCount; + private String routerKey; + + /** + * 创建路由配置 + */ + @Bean + public DBRouterConfig dbRouterConfig() { + DBRouterConfig config = new DBRouterConfig(); + config.setDbCount(dbCount); + config.setTbCount(tbCount); + config.setRouterKey(routerKey); + return config; + } + + /** + * 创建路由策略 + */ + @Bean + @ConditionalOnMissingBean + public IDBRouterStrategy dbRouterStrategy(DBRouterConfig dbRouterConfig) { + return new DBRouterStrategyHashCode(dbRouterConfig); + } + + /** + * 创建AOP切面 + */ + @Bean + @ConditionalOnMissingBean + public DBRouterJoinPoint point(DBRouterConfig dbRouterConfig, IDBRouterStrategy dbRouterStrategy) { + return new DBRouterJoinPoint(dbRouterConfig, dbRouterStrategy); + } + + /** + * 创建MyBatis插件 + */ + @Bean + @ConditionalOnMissingBean + public Interceptor plugin() { + return new DynamicMybatisPlugin(); + } + + /** + * 创建动态数据源 + */ + @Bean + @ConditionalOnMissingBean + public DataSource createDataSource() { + // 创建多个数据源 + Map targetDataSources = new HashMap<>(); + for (int i = 1; i <= dbCount; i++) { + String dbKey = String.format("db%02d", i); + DataSource dataSource = createDataSource(dataSourceMap.get(dbKey)); + targetDataSources.put(dbKey, dataSource); + } + + // 创建动态数据源 + DynamicDataSource dynamicDataSource = new DynamicDataSource(); + dynamicDataSource.setTargetDataSources(targetDataSources); + dynamicDataSource.setDefaultTargetDataSource(createDataSource(defaultDataSourceConfig)); + return dynamicDataSource; + } + + /** + * 创建单个数据源 + */ + private DataSource createDataSource(Map config) { + if (null == config || config.isEmpty()) { + return null; + } + + try { + // 使用HikariCP创建数据源 + com.zaxxer.hikari.HikariConfig hikariConfig = new com.zaxxer.hikari.HikariConfig(); + + // 设置基本属性 + hikariConfig.setJdbcUrl((String) config.get("url")); + hikariConfig.setUsername((String) config.get("username")); + hikariConfig.setPassword((String) config.get("password")); + hikariConfig.setDriverClassName((String) config.get("driver-class-name")); + + // 设置连接池属性 + if (config.containsKey(TAG_POOL)) { + Map poolConfig = (Map) config.get(TAG_POOL); + if (poolConfig.containsKey("minSize")) { + hikariConfig.setMinimumIdle((Integer) poolConfig.get("minSize")); + } + if (poolConfig.containsKey("maxSize")) { + hikariConfig.setMaximumPoolSize((Integer) poolConfig.get("maxSize")); + } + } + + return new com.zaxxer.hikari.HikariDataSource(hikariConfig); + } catch (Exception e) { + throw new RuntimeException("创建数据源失败", e); + } + } + + /** + * 创建事务模板 + */ + @Bean + public TransactionTemplate transactionTemplate(DataSource dataSource) { + return new TransactionTemplate(new DataSourceTransactionManager(dataSource)); + } + + @Override + public void setEnvironment(Environment environment) { + String prefix = "router.jdbc.datasource."; + + // 读取数据库数量 + dbCount = Integer.parseInt(environment.getProperty(prefix + "dbCount", "2")); + tbCount = Integer.parseInt(environment.getProperty(prefix + "tbCount", "4")); + routerKey = environment.getProperty(prefix + "routerKey", "userId"); + + // 读取全局配置 + Map globalProps = getGlobalProps(environment, prefix); + + // 读取每个数据源的配置 + for (int i = 1; i <= dbCount; i++) { + String dbKey = String.format("db%02d", i); + Map dbProps = PropertyUtil.handle(environment, prefix + dbKey, Map.class); + if (null != dbProps && !dbProps.isEmpty()) { + // 注入全局配置 + injectGlobal(globalProps, dbProps); + dataSourceMap.put(dbKey, dbProps); + } + } + + // 读取默认数据源配置 + defaultDataSourceConfig = PropertyUtil.handle(environment, prefix + "default", Map.class); + if (null != defaultDataSourceConfig && !defaultDataSourceConfig.isEmpty()) { + injectGlobal(globalProps, defaultDataSourceConfig); + } + } + + /** + * 获取全局配置 + */ + private Map getGlobalProps(Environment environment, String prefix) { + try { + Map globalProps = PropertyUtil.handle(environment, prefix + TAG_GLOBAL, Map.class); + return null == globalProps ? new HashMap<>() : globalProps; + } catch (Exception e) { + return new HashMap<>(); + } + } + + /** + * 注入全局配置到数据源配置 + */ + private void injectGlobal(Map globalProps, Map dbProps) { + if (null == globalProps || globalProps.isEmpty()) { + return; + } + for (Map.Entry entry : globalProps.entrySet()) { + if (!dbProps.containsKey(entry.getKey())) { + dbProps.put(entry.getKey(), entry.getValue()); + } + } + } +} +``` + +--- + +## 📝 配置示例 + +### application.yml配置 + +```yaml +router: + jdbc: + datasource: + dbCount: 2 + tbCount: 4 + routerKey: userId + global: + driver-class-name: com.mysql.jdbc.Driver + pool: + minSize: 5 + maxSize: 20 + db01: + url: jdbc:mysql://localhost:3306/db01?useUnicode=true&characterEncoding=UTF-8 + username: root + password: 123456 + db02: + url: jdbc:mysql://localhost:3306/db02?useUnicode=true&characterEncoding=UTF-8 + username: root + password: 123456 + default: + url: jdbc:mysql://localhost:3306/default?useUnicode=true&characterEncoding=UTF-8 + username: root + password: 123456 +``` + +--- + +## 🎓 知识点拓展 + +### 拓展1:数据源连接池选择 + +**HikariCP**: +- ✅ 性能最好 +- ✅ Spring Boot 2.x默认 +- ✅ 轻量级 + +**Druid**: +- ✅ 功能强大(监控、SQL解析) +- ✅ 适合生产环境 +- ❌ 相对重一些 + +**选择建议**: +- 性能优先:HikariCP +- 功能优先:Druid + +### 拓展2:配置读取的优先级 + +**优先级**(从高到低): +1. 数据源特定配置(db01.url) +2. 全局配置(global.url) +3. 默认值 + +**实现**:使用injectGlobal方法,数据源配置优先 + +--- + +## ✅ 今日检查清单 + +- [ ] 完善了DataSourceAutoConfig类 +- [ ] 实现了数据源创建逻辑 +- [ ] 实现了配置读取 +- [ ] 实现了getGlobalProps和injectGlobal方法 +- [ ] 理解了数据源连接池配置 +- [ ] 完成了拓展阅读 + +--- + +## 🎯 明日预告 + +明天我们将创建spring.factories文件,完成自动配置的入口。 + +--- + +## 💡 思考题 + +1. 为什么需要全局配置和数据源特定配置? +2. 如何支持其他连接池(如Druid)? +3. 配置读取的优先级是什么? + +--- + +## 📚 参考资源 + +- [HikariCP文档](https://github.com/brettwooldridge/HikariCP) +- [Spring Boot数据源配置](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html#data.sql.datasource) +- [多数据源配置](https://www.baeldung.com/spring-boot-configure-multiple-datasources) diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 261eeb9..0000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -- GitLab