提交 2a32df78 编写于 作者: I isczj

docs(30天实现指南): 添加数据库路由SpringBoot Starter学习指南

- 创建README.md包含30天学习计划和使用说明
- 添加完整项目代码清单文档
- 创建第01天项目初始化和Maven配置教学文件
- 创建第02天数据库路由和分库分表概念教学文件
- 提供详细的学习路径和实践任务
- 包含知识点拓展和思考题内容
上级 98db1cad
# 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、反射、注解等核心概念
- ✅ 能够独立开发类似的中间件
- ✅ 知其然,知其所以然
---
**祝你学习顺利!有问题随时记录,每天进步一点点!** 🎉
# 完整项目代码清单
## 📚 说明
本文档列出了实现 `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. **拓展思考**:完成拓展练习,加深理解
---
## 💡 提示
- 所有代码都在对应的日期文件中
- 每个文件都有详细的代码和解释
- 遇到问题可以查看对应日期的文件
- 完成所有文件后,项目就可以使用了
---
**祝你学习顺利!** 🎉
# 第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
<!-- 传统方式:需要引入很多依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<!-- ... 还有很多 -->
<!-- Spring Boot方式:一个依赖搞定 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 自动包含了所有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
<!-- 引入这个依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- 自动就有了:
- SqlSessionFactory
- SqlSessionTemplate
- Mapper扫描
- 等等...
-->
```
### 我们的目标
创建一个 `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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 项目坐标 -->
<groupId>cn.bugstack.middleware</groupId>
<artifactId>db-router-spring-boot-starter</artifactId>
<version>1.0.2</version>
<packaging>jar</packaging>
<!-- 继承Spring Boot父项目 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- Spring Boot 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot 自动配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- Spring Boot 配置处理器(用于IDE提示) -->
<!--
可选依赖:只是用于IDE自动提示配置属性
原因:optional=true,不会传递到用户项目
建议:开发时保留,可以让IDE提示配置项;如果不需要提示,可以注释掉
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<!-- MySQL驱动 -->
<!--
可选依赖:用户项目会自己引入数据库驱动
原因:我们的starter不直接使用MySQL,只是示例配置中用到
建议:如果只是学习,可以注释掉;如果要测试,需要保留
-->
<!--
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
</dependency>
-->
<!-- Apache Commons BeanUtils(用于反射获取属性) -->
<!-- 必须:PropertyUtil.getProperty()方法使用 -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
<!-- Apache Commons Lang(字符串工具) -->
<!-- 必须:StringUtils.isBlank()等方法使用 -->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<!-- FastJSON(JSON处理) -->
<!--
可选依赖:代码中没有使用FastJSON
原因:检查了所有代码文件,没有找到fastjson的使用
可以安全删除
-->
<!--
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
-->
<!-- 测试依赖 -->
<!--
可选依赖:只在测试时需要
原因:scope=test,不会打包到最终jar中
建议:如果要写测试,保留;如果只是学习核心功能,可以注释掉
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--
JUnit依赖:可选
原因:spring-boot-starter-test已经包含了JUnit
可以删除,避免重复
-->
<!--
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
-->
</dependencies>
<build>
<finalName>db-router-spring-boot-starter</finalName>
<plugins>
<!-- Maven编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- 源码打包插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
```
### 依赖说明
**📋 依赖分类总结**
**✅ 必须依赖(核心功能需要)**
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
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
```
---
#### 1. Spring Boot Starter
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
```
**作用**:Spring Boot核心功能,包含自动配置、日志等
#### 2. Spring Boot Autoconfigure
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
```
**作用**:自动配置的核心,我们用它来实现自动配置
#### 3. Spring Boot AOP
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
```
**作用**:AOP功能,用于拦截方法,实现路由逻辑
#### 4. MyBatis Spring Boot Starter
```xml
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
```
**作用**:MyBatis集成,我们需要拦截SQL修改表名
#### 5. Commons BeanUtils
```xml
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</dependency>
```
**作用**:通过反射获取对象属性值(如获取userId)
**是否必须**:✅ 必须 - PropertyUtil.getProperty()方法使用
#### 6. Commons Lang
```xml
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
```
**作用**:字符串工具类(isBlank等方法)
**是否必须**:✅ 必须 - StringUtils使用
#### 7. MySQL驱动
**是否必须**:❌ 可选 - 用户项目会自己引入数据库驱动
#### 8. FastJSON
**是否必须**:❌ 可选 - 代码中没有使用,可以删除
#### 9. 测试依赖
**是否必须**:❌ 可选 - 只在测试时需要(scope=test)
---
## 🎓 知识点拓展
### 拓展1:Maven依赖作用域(Scope)
```xml
<scope>compile</scope> <!-- 默认,编译和运行时都需要 -->
<scope>provided</scope> <!-- 编译时需要,运行时由容器提供(如servlet-api) -->
<scope>runtime</scope> <!-- 运行时需要,编译时不需要(如JDBC驱动) -->
<scope>test</scope> <!-- 只在测试时需要 -->
<scope>system</scope> <!-- 系统路径,不推荐 -->
```
**为什么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
<groupId>cn.bugstack.middleware</groupId> <!-- 组织/公司 -->
<artifactId>db-router-spring-boot-starter</artifactId> <!-- 项目名 -->
<version>1.0.2</version> <!-- 版本号 -->
```
**命名规范**
- groupId:通常是域名倒写(如com.company.project)
- artifactId:项目名,小写,用连字符
- version:语义化版本(主版本.次版本.修订版本)
---
## ✅ 今日检查清单
- [ ] 创建了完整的项目目录结构
- [ ] 创建了pom.xml文件
- [ ] 理解了每个依赖的作用
- [ ] 能够解释为什么需要这些依赖
- [ ] 完成了拓展阅读
---
## 🎯 明日预告
明天我们将学习:
- 什么是数据库路由
- 为什么需要分库分表
- 路由的基本原理
---
## 💡 思考题
1. 如果不用Spring Boot Starter,用户需要手动配置哪些东西?
2. 为什么starter的依赖要用`optional=true`
3. Maven的`<parent>`标签有什么作用?
---
## 📚 参考资源
- [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)
# 第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)
# 第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)
# 第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 <T> 泛型
* @return 属性对象
*/
public static <T> T handle(Environment environment, String prefix, Class<T> 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/)
# 第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)
# 第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<String, Map<String, Object>> dataSourceMap = new HashMap<>();
private Map<String, Object> 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<Object, Object> 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<String, Object> 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)
# 第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)
# 第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<String> 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<String> dbKey = new ThreadLocal<>();
private static final ThreadLocal<String> 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<String> 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<String> 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)
# 第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<Object, Object> 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<Object, Object> 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)
# 第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)
# 第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> T handle(Environment environment, String prefix, Class<T> 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/)
# 第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)
# 第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, DBRouter> 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)
# 第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)
# 第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)
---
**祝你学习顺利!有问题随时记录,每天进步一点点!** 🎉
# 第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<String, Map<String, Object>> dataSourceMap = new HashMap<>();
private Map<String, Object> 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<Object, Object> 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<String, Object> 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<String, Object> poolConfig = (Map<String, Object>) 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<String, Object> globalProps = getGlobalProps(environment, prefix);
// 读取每个数据源的配置
for (int i = 1; i <= dbCount; i++) {
String dbKey = String.format("db%02d", i);
Map<String, Object> 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<String, Object> getGlobalProps(Environment environment, String prefix) {
try {
Map<String, Object> 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<String, Object> globalProps, Map<String, Object> dbProps) {
if (null == globalProps || globalProps.isEmpty()) {
return;
}
for (Map.Entry<String, Object> 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)
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.
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册