前言
一般我们开发的单体项目中,都是一个前端,一个后端,一个数据库。但在实际的应用开发中,有时候,一个后端会同时用到多个数据库。这时候可能就会需要用到动态数据源。
之前公司有一个类似的业务,这是一个数据处理的系统,后端会接收不同类型的数据,不同的数据,要根据不同的数据类型,存储查询到不同的数据库中,当时就是通过使用Spring动态数据源+aop进行实现的。
在spring动态数据源配置中,涉及到的类有:
DriverManagerDataSource 负责封装数据连接的参数,数据库的url,username和password,在多数据源的配置中,spring会管理多个DriverManagerDataSource 的对象,对象存储到AbstractRoutingDataSource 类中,通过key-value的形式进行存储。
AbstractRoutingDataSource 是实现动态数据源的关键,通过实现这个类的determineCurrentLookupKey()方法,决定了当线程进入后,jdbc调用哪个数据库,实现类交给spring管理,并为其配置多个数据源,
ThreadLocal 存储线程的数据源key,通过key去找到对应的datasource
切面,通过自定义注解定义切面类,在注解中,通过参数灵活的定义接口访问的数据源。
我们想连接一个数据库,在JDBC中,我们需要为JDBC提供数据库的个url,userName,和password三个参数:如
jdbc:mysql://localhost:3306/t_db1?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
有了这三个连接数据库的参数后,我们就能用这三个参数,构建DataSource类,这是个接口,在spring中的继承关系如下
文件描述有点苍白,建议结合demo代码去理解。
创建三个不同的数据库,在数据库中创建三张相同的表,表里面随便写几条数据进去。
-- auto-generated definition
create table t_test
(
id int auto_increment
primary key,
name text null,
age double null
);
一般是在application里面去定义我们的数据源配置,数据源的配置比如说datasource01,default是我们自己定义的参数,并不是springboot内置的。default代表默认的数据源,即没有使用注解,找不到对应数据源时应该使用哪个。这里要注意,为什么值是dataSource3而不是dataSource03,这个后面再说,并不是写错了。
spring:
datasource:
# 可以自己定义
datasource01:
username: root
password: xxx
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/t_db1?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
datasource02:
username: root
password: xxx
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/t_db2?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
datasource03:
username: root
password: xxx
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/t_db3?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
default: dataSource3
这个没啥好说的
public class TestBean {
private Integer id;
private String name;
private Double age;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getAge() {
return age;
}
public void setAge(Double age) {
this.age = age;
}
@Override
public String toString() {
return "TestBean{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
通过@Configuration注解和@ConfigurationProperties注解,让spring能读到装载了参数的bean。
这里粘一个就行了,剩下的自己补充。
@Data
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.datasource01")
public class DataSource01 {
String url;
String userName;
String password;
}
为什么要使用ThreadLocal呢?因为在服务器端,JDBC只有一份,我们需要动态的让JDBC使用某一个数据源,所以,在不同线程进入服务的时候,把线程所需要使用的数据源的信息,通过ThreadLocal进行隔离。
public class DataSourceKeyHolder {
private static final ThreadLocal<String> keyHolder = new ThreadLocal<>();
public static void setKey(String key) {
keyHolder.remove();
keyHolder.set(key);
}
public static String getKey() {
String s = keyHolder.get();
return s;
}
public static void clear(){
keyHolder.remove();
}
}
继承这个类是1关键,动态数据源就是通过调用determineCurrentLookupKey确定数据源的bean名称,这个类将会给spring管理。
public class MyDynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceKeyHolder.getKey();
}
}
注意下面管理的Bean有哪些,分别是三个数据库数据源的实体类,MyDynamicDataSource类,这个类通过map的形式,装载数据源实体。JdbcTemplate,JDBC通过参数AbstractRoutingDataSource类,确定数据源。
这里注意的是,数据源实体类的方法名,就是配置文件里面设置默认数据源里配置。
@Configuration
public class Config {
@Autowired
private DataSource01 dataSource1;
@Autowired
private DataSource02 dataSource2;
@Autowired
private DataSource03 dataSource3;
@Value("${spring.datasource.default}")
private String def;
@Bean
public DriverManagerDataSource dataSource1(){
String url = dataSource1.getUrl();
String userName = dataSource1.getUserName();
String password = dataSource1.getPassword();
return new DriverManagerDataSource(url,userName,password);
}
@Bean
public DriverManagerDataSource dataSource2(){
String url = dataSource2.getUrl();
String userName = dataSource2.getUserName();
String password = dataSource2.getPassword();
return new DriverManagerDataSource(url,userName,password);
}
@Bean
public DriverManagerDataSource dataSource3(){
String url = dataSource3.getUrl();
String userName = dataSource3.getUserName();
String password = dataSource3.getPassword();
return new DriverManagerDataSource(url,userName,password);
}
@Bean
public MyDynamicDataSource dynamicDataSource(Map<String,DriverManagerDataSource> map){
HashMap<Object, Object> original = new HashMap<>(map);
MyDynamicDataSource myDynamicDataSource = new MyDynamicDataSource();
myDynamicDataSource.setDefaultTargetDataSource(map.get(def));
myDynamicDataSource.setTargetDataSources(original);
return myDynamicDataSource;
}
@Bean
public JdbcTemplate jdbcTemplate(AbstractRoutingDataSource dataSource){
return new JdbcTemplate(dataSource);
}
}
/**
* 类描述: 设置拦截数据源的注解,可以设置在具体的类上,或者在具体的方法上
*
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
/**
* 切换数据源名称
*/
String value() default "source2";
}
@Aspect
@Order(1)
@Component
public class DataSourceAspect {
@Pointcut("@annotation(com.example.dynamicdemo.dynamic.asp.DataSource)")
public void dsPointCut() {
}
@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DataSource dataSource = method.getAnnotation(DataSource.class);
if (dataSource != null) {
DataSourceKeyHolder.setKey(dataSource.value());
}
try {
return point.proceed();
} finally {
// 销毁数据源 在执行方法之后
DataSourceKeyHolder.clear();
}
}
}
@RestController
public class TestController {
@Autowired
JdbcTemplate jdbcTemplate;
@GetMapping("/source1")
@DataSource(value = "dataSource1")
public Object local(){
TestBean testBean = jdbcTemplate.queryForObject("select * from t_test where id=?", new BeanPropertyRowMapper<>(TestBean.class), 1);
return testBean;
}
@GetMapping("/source2")
@DataSource(value = "dataSource2")
public List<Map<String, Object>> remote(){
List<Map<String, Object>> maps = jdbcTemplate.queryForList("select * from t_test");
return maps;
}
@GetMapping("/source3")
@DataSource(value = "dataSource3")
public List<Map<String, Object>> remote1(){
List<Map<String, Object>> maps = jdbcTemplate.queryForList("select * from t_test");
return maps;
}
@GetMapping("/source4")
public List<Map<String, Object>> remote4(){
List<Map<String, Object>> maps = jdbcTemplate.queryForList("select * from t_test");
return maps;
}
}
接口中的方法,sql内容都是相同的,但是访问每个接口的结果却不相同。
很明显,他们访问的数据库并不相同。