自定义多数据源
SpringBoot利用自定义注解实现多数据源,前置知识:注解、Aop、SpringBoot整合Mybaits
1、搭建工程
创建一个SpringBoot工程,并引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 解析多数据源注解 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.18</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2、定义多数据源注解
/**
* 1、定义多数据源注解
* @author ss_419
* TODO 这个注解将来可以加在service类上或者方法上,通过value属性来指定类或者方法应该使用那个数据源
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface DataSource {
String value() default DataSourceType.DEFAULT_DS_NAME;
}
3、创建一个多数据上下文对象
这个类用来存储当前线程所使用的数据源名称
/**
* TODO 这个类用来存储当前线程所使用的数据源名称
*
* @author ss_419
* @version 1.0
* @date 2023/5/21 09:21
*/
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
CONTEXT_HOLDER.set(dataSourceType);
}
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
4、配置aop
-
@annotation(org.pp.dd.annotation.DataSource) 如果有@DataSource注解就给拦截下来
-
@within(org.pp.dd.annotation.DataSource) 表示类上有@DataSource注解就将类中的方法给拦截下来
/**
* TODO
*
* @author ss_419
* @version 1.0
* @date 2023/5/21 09:42
*/
@Component
@Aspect
@Order(11)
public class DataSourceAspect {
/**
* 定义切点
*
* @annotation(org.pp.dd.annotation.DataSource) 如果有@DataSource注解就给拦截下来
* @within(org.pp.dd.annotation.DataSource) 表示类上有@DataSource注解就将类中的方法给拦截下来
*/
@Pointcut("@annotation(org.pp.dd.annotation.DataSource) || @within(org.pp.dd.annotation.DataSource)")
public void pc() {
}
/**
* 环绕通知
*
* @param pjp
* @return
*/
@Around("pc()")
public Object around(ProceedingJoinPoint pjp) {
// 获取方法上的有效注解
DataSource dataSource = getDataSource(pjp);
if (dataSource != null) {
// 获取注解中数据源的名称
String value = dataSource.value();
DynamicDataSourceContextHolder.setDataSourceType(value);
}
try {
return pjp.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
} finally {
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
private DataSource getDataSource(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
// 获取方法上的注解
DataSource annotation = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
if (annotation != null) {
// 说明方法上有注解
return annotation;
}
return (DataSource) AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
}
}
5、读取参数DruidProperties
/**
* TODO 读取数据源
*
* @author ss_419
* @version 1.0
* @date 2023/5/21 10:20
*/
@ConfigurationProperties(prefix = "spring.datasource")
public class DruidProperties {
private String type;
private String driverClassName;
private Map<String, Map<String ,String>> ds;
private Integer initialSize;
private Integer minIdle;
private Integer maxActive;
private Integer maxWait;
/**
* 在这个方法中设置公共属性
* @param dataSource
* @return
*/
public DataSource dataSource(DruidDataSource dataSource){
dataSource.setInitialSize(initialSize);
dataSource.setMinIdle(minIdle);
dataSource.setMaxActive(maxActive);
dataSource.setMaxWait(maxWait);
return dataSource;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getDriverClassName() {
return driverClassName;
}
public void setDriverClassName(String driverClassName) {
this.driverClassName = driverClassName;
}
public Map<String, Map<String, String>> getDs() {
return ds;
}
public void setDs(Map<String, Map<String, String>> ds) {
this.ds = ds;
}
public Integer getInitialSize() {
return initialSize;
}
public void setInitialSize(Integer initialSize) {
this.initialSize = initialSize;
}
public Integer getMinIdle() {
return minIdle;
}
public void setMinIdle(Integer minIdle) {
this.minIdle = minIdle;
}
public Integer getMaxActive() {
return maxActive;
}
public void setMaxActive(Integer maxActive) {
this.maxActive = maxActive;
}
public Integer getMaxWait() {
return maxWait;
}
public void setMaxWait(Integer maxWait) {
this.maxWait = maxWait;
}
}
6、加载数据源LoadDataSource
/**
* TODO 加载数据源
*
* @author ss_419
* @version 1.0
* @date 2023/5/21 10:30
*/
@Component
@EnableConfigurationProperties(DruidProperties.class)
public class LoadDataSource {
@Autowired
DruidProperties druidProperties;
public Map<String, DataSource> loadAllDataSource() {
Map<String, DataSource> map = new HashMap<>();
Map<String, Map<String, String>> ds = druidProperties.getDs();
try {
Set<String> keySet = ds.keySet();
for (String key : keySet) {
map.put(key, druidProperties.dataSource((DruidDataSource) DruidDataSourceFactory.createDataSource(ds.get(key))));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return map;
}
}
7、定义数据源管理器
当系统需要调用数据源的时候,数据源以key-value存起来,当需要数据源时调用determineCurrentLookupKey()方法来获取数据源。
由于本人实力原因,解答不了大家这里的疑惑。大致功能 通过修改本地线程的值,来实现数据源的切换。
/**
* TODO 设置数据源
* 当系统需要调用数据源的时候,数据源以key-value存起来,当需要数据源时调用determineCurrentLookupKey()方法
* @author ss_419
* @version 1.0
* @date 2023/5/21 10:47
*/
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(LoadDataSource loadDataSource) {
//1、设置所有的数据源
Map<String, DataSource> allDs = loadDataSource.loadAllDataSource();
super.setTargetDataSources(new HashMap<>(allDs));
//2、设置默认数据源
super.setDefaultTargetDataSource(allDs.get(DataSourceType.DEFAULT_DS_NAME));
super.afterPropertiesSet();
}
/**
* 这个方法用来返回数据源名称,当系统需要获取数据源的时候会自动调用该方法获取数据源名称
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
定一个用于存储数据库类型的接口,这个接口类似于枚举类:
/**
* TODO
*
* @author ss_419
* @version 1.0
* @date 2023/5/21 10:54
*/
public interface DataSourceType {
String DEFAULT_DS_NAME = "master";
String DS_SESSION_KEY = "ds_session_key";
}
8、测试
创建User实体:
/**
* TODO
*
* @author ss_419
* @version 1.0
* @date 2023/5/21 11:15
*/
public class User {
private Integer id;
private String username;
private String password;
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
创建UserService:
@Service
// 在类上加注解的效果,会使该类的所有方法都切入到新的数据源中
//@DataSource
public class UserService {
@Autowired
UserMapper userMapper;
// 在方法上加注解的效果,只会让指定的方法切入到另一个数据源中
//@DataSource("slave")
public List<User> findUsers(){
return userMapper.findAllUsers();
}
}
创建UserMapper:
@Mapper
public interface UserMapper {
@Select("SELECT * FROM user")
List<User> findAllUsers();
}
测试类:
@SpringBootTest
class DynamicDatasourcesApplicationTests {
@Autowired
UserService userService;
@Test
void contextLoads() {
List<User> users = userService.findUsers();
users.stream()
.forEach(user -> System.out.println(user));
}
}
默认选择主库的数据源:
执行结果如下:
在Service上加上注解,指定数据源为从库:
执行结果如下: