涉及到的注解
@ConfigurationProperties(prefix = “spring.datasource”)类型安全的属性注入,prefix = "spring.datasource"是他的前缀
@Component,注入到spring容器
@EnableConfigurationProperties({DynamicDataSourceProperties.class}),引入数据,属性类注入特殊方式
@Autowired,手动注入数据,多看看底层源码他们调用构造注入,的用法。
@Getter,@Setter 来自lombook的set,get方法
@EnableConfigurationProperties({DynamicDataSourceProperties.class})引入数据,属性类注入特殊方式
两个前置知识点:
- 在 SpringMVC 中,一个请求到达 Servlet 之后,会自动的分配一个线程去处理当前请求。在当前请求处理结束之前,从 Controller 到 Service 到 Dao/Mapper,统一都是一个线程在处理。
- 当系统需要使用数据源的时候(无论是 JdbcTemplate 还是 Jpa 还是 MyBatis),都是去调用 AbstractRoutingDataSource 对象的方法去获取数据源。这个对象中默认会保存一个或者多个数据源,如果保存了多个数据源,那么每个数据源都会有一个名字。在需要的时候,直接调用 determineCurrentLookupKey 方法去获取数据源的名字,进而获取到对应的数据源。
实现思路:
- 自定义一个 @Ds 注解,如果哪个方法需要标记数据源就给哪个方法加上这个注解。
- 自定义 AOP 切面,切点就是加了 @Ds 注解的方法。
- 凡是加了 @Ds 注解的方法,统统在执行之前就会被拦截下来,拦截下来之后,读取到这个方法上的 @Ds 注解,并且获取到注解中所标记的数据源的名称,并设置给 ThreadLocal 对象,然后继续执行被拦截下来的方法,被拦截的方法在执行的过程中,需要用到数据源,就去调用 com.qfedu.dynamic_datasource.dd.DynamicDataSource#determineCurrentLookupKey 方法去获取数据源的名称,结果就会获取到刚刚设置的数据源。
一.加入依赖
建立一个普通的springBoot项目,如下选择依赖
再加入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.11</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
二.配置application.yaml
注意:
1.空格
2.主机名必须为master,从机都行
3. mysql 8.0 :driver-class-name: com.mysql.cj.jdbc.Driver
mysql5.0: driver-class-name: com.mysql.jdbc.Driver
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
ds:
# 配置主机,假设主机必须叫 master
master:
username: root
password: 1234
url: jdbc:mysql:///test01?serverTimezone=Asia/Shanghai
# 配置从机,从机名字无所谓
slave:
username: root
password: 1234
url: jdbc:mysql:///test02?serverTimezone=Asia/Shanghai
三.设计数据库
配置两个数据库
数据库一的book表进行插入操作
数据库二的book表进行读写操作
四.开始编写我们的项目
4.1 DynamicDataSourceProperties来加载yaml配置文件的内容
要点:这个类是为了来加载yaml的配置文件,那么怎么去加载?,通过这个安全配置类
package com.huang.dynamic_datasoruce_huang.dd;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.Map;
@Getter
@Setter
//第一步
//这个类是为了来加载yaml的配置文件,那么怎么去加载?,通过这个安全配置类
@ConfigurationProperties(prefix = "spring.datasource")
public class DynamicDataSourceProperties {
private String type;
private String driverClassName;
private Map<String,String> ds;
}
4.2 通过DynamicDataSourceProvider接口来区分数据来自主表还是从表
package com.huang.dynamic_datasoruce_huang.dd;
import java.util.Map;
public interface DynamicDataSourceProvider {
//这里是第二步
//我们加载了DynamicDataSourceProperties配置类,把yaml的内容加载进来,但是数据有主表和从表,我们要怎么去获取?
//答案:用DynamicDataSourceProvider这个类来区分数据源是来自主表还是从表
//2.1定义数据源名字
String DEFAULT_DATASOURCE_NAME="master";
//2.2加载数据源
Map<Object,Object> loadDataSources();
}
4.3来加载数据源
package com.huang.dynamic_datasoruce_huang.dd;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
//注入到spring容器
@Component
//引入数据,属性类注入特殊方式
@EnableConfigurationProperties({DynamicDataSourceProperties.class})
//实现接口,调用他的方法
public class YamlDynamicDataSourceProvider implements DynamicDataSourceProvider {
@Autowired
DynamicDataSourceProperties dynamicDataSourceProperties;
@Override
public Map<Object, Object> loadDataSources() {
/*3.
* 这里来加载数据源
* 关键点:
* 3.1 yaml里面的都是字符串,我们要在这里把字符串转换成datasource对象,
* 而对象又分为两种,即是我们第二步定义的,master和cong两种
* 而map里面存的数据看我们第二步定义的类里面有多少条
*
*
* */
//3.2创建返回对象,map里面存的数据看我们第二步定义的类里面有多少条
Map<Object, Object> map = new HashMap<>(dynamicDataSourceProperties.getDs().size());
Map<String, Map<String, String>> ds = dynamicDataSourceProperties.getDs();
Set<String> keySet = ds.keySet();
try {
for (String key : keySet) {
Map<String, String> dsMap = ds.get(key);
/*
3.3ds拿到的对象是master和slave,以map的形式传入DruidDataSourceFactory.createDataSource(dsMap)方法中
,并且给你创建一个dataSource数据源
*/
DataSource dataSource = DruidDataSourceFactory.createDataSource(dsMap);
/*
* 3.4 再把数据源传入到DataSource中,这样子就完成了,把yaml里面的数据传入到数据源中
* */
map.put(key, dataSource);
}
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
}
4.4 定义一个DynamicDataSource
1.以后无论是mybatis,jpa还是jdbctemplate,只要需要数据源,都在这个determineCurrentLookupKey,方法中去进行查找数据源
package com.huang.dynamic_datasoruce_huang.dd;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/*
* DynamicDataSourc定义一个动态数据源,以后无论是
* mybatis,jpa还是jdbctemplate,只要需要数据源,都在这个
* determineCurrentLookupKey,方法中去进行查找数据源
* */
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {
DynamicDataSourceProvider dynamicDataSourceProvider;
public DynamicDataSource(DynamicDataSourceProvider dynamicDataSourceProvider) {
this.dynamicDataSourceProvider = dynamicDataSourceProvider;
//将所有的数据源,都设置给 AbstractRoutingDataSource
super.setTargetDataSources(dynamicDataSourceProvider.loadDataSources());
//设置默认的数据源
//将 master 设置为默认的数据源
super.setDefaultTargetDataSource(dynamicDataSourceProvider.loadDataSources().get(DynamicDataSourceProvider.DEFAULT_DATASOURCE_NAME));
}
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDsType();
}
}
4.5创造拦截器DynamicDataSourceContextHolder,利用ThreadLocal来拦截方法
package com.huang.dynamic_datasoruce_huang.dd;
public class DynamicDataSourceContextHolder {
/**
* 第四步:定义拦截器
* ThreadLocal 就是一个本地线程变量,在这个线程中保存的数据,必须在这个线程中读取
* ,换一个线程就读不出来了
* <p>
* 在 Servlet 中,当一个请求到达的时候,会自动从线程池中拿一个线程出来处理当前请求,
* 处理完毕之后,当前线程会被放回线程池,等待下一次使用。
* <p>
* 前端发一个请求来,要执行某一个 service 层方法的时候,我们将这个请求拦截下来,
* 拦截下来之后,根据方法的注解,设置一个数据源,将这个数据源存到 ThreadLocal 中,
* 在后续执行的过程中,当系统需要数据源的时候,就从 ThreadLocal 中返回。对于同一个请求来说,
* 获取到的始终是同一个数据源。
*/
/*因为,这个线程同一时间只能存在一个数据,所以没必要传map进来,我们用string类型丢进来就好了
,如果想让很多数据同时存储,就传入map
**/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDsType(String dsType) {
CONTEXT_HOLDER.set(dsType);
}
public static String getDsType() {
return CONTEXT_HOLDER.get();
}
public static void removeDsType() {
CONTEXT_HOLDER.remove();
}
}
4.6定义一个自定义注解ds,来区分数据源
注意:
1.自定义一个数据源注解,用来区分数据源,这样会有侵入,用xml配置拦截器没有侵入但是不好用(拦截所有的方法),没办法区分我们想要的数据源
2.将来用户在使用这个注解的时候,通过 value 属性去指定想要使用哪个数据源,默认是 master
package com.huang.dynamic_datasoruce_huang.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义一个数据源注解,用来区分数据源,这样会有侵入,
* 用xml配置拦截器没有侵入但是不好用(拦截所有的方法),没办法区分我们想要的数据源
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface Ds {
/**
* 将来用户在使用这个注解的时候,通过 value 属性去指定想要使用哪个数据源,默认是 master
* @return
*/
String value() default "master";
}
4.7定义数据源的切点(进行数据源的查找)
1.去DynamicDataSource里面查找数据源
package com.huang.dynamic_datasoruce_huang.dd;
import com.huang.dynamic_datasoruce_huang.annotation.Ds;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
@Component
/*
@Aspect
@EnableAspectJAutoProxy*/
public class DataSourceAspect {
/**
* 哪个方法上添加了 @Ds 注解,就把哪个方法拦截下来
*
* 或者某个类上面加了这个注解,就将这个类中的所有方法都拦截下来
*/
@Pointcut("@annotation(com.huang.dynamic_datasoruce_huang.annotation.Ds) || @within(com.huang.dynamic_datasoruce_huang.annotation.Ds)")
public void pointcut(){}
/**
* 在目标方法执行之前,把数据源切换过来
* 在目标方法执行之后,还需要清空 ThreadLocal,否则会造成oom
* 去DynamicDataSource里面查找数据源
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Ds ds = getDs(pjp);
//获取被拦截下来的方法所需要的数据源
String value = ds.value();
//这里的异常要抛出去,方便以后service层可以调用异常进行事务回滚
try {
//设置数据源
DynamicDataSourceContextHolder.setDsType(value);
//执行目标方法
return pjp.proceed();
} finally {
//最终移除数据源
DynamicDataSourceContextHolder.removeDsType();
}
}
/**
* 首先去方法上获取,方法上如果有这个注解,就直接返回,方法上要是没有这个注解,那就去类上面获取
* @param pjp
* @return
*/
private Ds getDs(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
//查找某一个方法上的注解
//第一个参数就是目标方法(被拦截下来的方法)
//第二个参数是注解
//返回值不为 null,说明找到注解了,返回值为null,说明没找到注解
Ds annotation = AnnotationUtils.findAnnotation(signature.getMethod(), Ds.class);
if (annotation == null) {
//说明方法上没有注解
annotation = AnnotationUtils.findAnnotation(signature.getDeclaringType(), Ds.class);
}
return annotation;
}
}