自定义多数据源
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上加上注解,指定数据源为从库:
执行结果如下: