项目背景:最近公司中需要搭建mysql的主从,想着在spring中集成多数据源。mybatisplus提供的有插件用@DS注解就能够实现,但是这种在mysql服务宕机的情况下不能够进行自动切换,于是就想着用aop+自定义注解的方式来实现
项目实现效果:如果公司服务器搭建的是一主多从多个mysql数据源,主服务器用来读。从服务器用来写。此时你在代码层面用注解指定了一个增删改方法到从数据源,但是碰巧此时从数据源失效了,那么就会自动的切换到其它服务器。代码实现如下:
注意:为了节省篇幅,向controller、service层就不展示出来了,只展示相关核心代码。
1、pom文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- aop 切面 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- druid -->
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<!--主从配置依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>2.4.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<!--mybatis-plus生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<!-- 模板引擎 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.14</version>
</dependency>
2、配置文件:application.yml
server:
port: 8088
spring:
datasource:
druid:
type: com.alibaba.druid.pool.DruidDataSource
master:
url: jdbc:mysql://192.168.26.4:3306/test01?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
slave:
url: jdbc:mysql://192.168.26.8:3306/test01?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
3、数据源名称枚举类CommonConstant:
public class CommonConstant {
/**
* 默认数据源标识
*/
public static final String MASTER = "master";
/**
* 从数据源标识
*/
public static final String SLAVE = "slave";
}
4 数据源解析类DruidConfig:
@Data
@Configuration
public class DruidConfig {
@Bean(name = CommonConstant.MASTER)
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource()
{
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return dataSource;
}
@Bean(name = CommonConstant.SLAVE)
@ConfigurationProperties("spring.datasource.druid.slave")
public DataSource slaveDataSource()
{
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return dataSource;
}
@Bean
@Primary
public DynamicDataSource dynamicDataSource()
{
Map<Object, Object> dataSourceMap = new HashMap<>(2);
dataSourceMap.put(CommonConstant.MASTER,masterDataSource());
dataSourceMap.put(CommonConstant.SLAVE,slaveDataSource());
//设置动态数据源
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
dynamicDataSource.setTargetDataSources(dataSourceMap);
//将数据源信息备份在defineTargetDataSources中
dynamicDataSource.setDefineTargetDataSources(dataSourceMap);
return dynamicDataSource;
}
}
5、DynamicDataSource类
编写DynamicDataSource类继承AbstractRoutingDataSource类并重写抽象方法determineCurrentLookupKey以此来决定当前线程使用哪个数据源
/**
* 动态数据源
* 调用AddDefineDataSource组件的addDefineDynamicDataSource()方法,获取原来targetdatasources的map,并将新的数据源信息添加到map中,并替换targetdatasources中的map
* 切换数据源时可以使用@DataSource(value = "数据源名称"),或者DynamicDataSourceContextHolder.setContextKey("数据源名称")
* @author zhangyu
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DynamicDataSource extends AbstractRoutingDataSource {
//备份所有数据源信息,
private Map<Object, Object> defineTargetDataSources;
/**
* 决定当前线程使用哪个数据源
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.getDynamicDataSourceKey();
}
}
6、DynamicDataSourceHolder
DynamicDataSourceHolder类主要是设置当前线程的数据源名称,移除数据源名称,以及获取当前数据源的名称,便于动态切换
/**
* 数据源切换处理
*
* @author zhangyu
*/
@Slf4j
public class DynamicDataSourceHolder {
/**
* 保存动态数据源名称
*/
private static final ThreadLocal<String> DYNAMIC_DATASOURCE_KEY = new ThreadLocal<>();
/**
* 设置/切换数据源,决定当前线程使用哪个数据源
*/
public static void setDynamicDataSourceKey(String key){
log.info("数据源切换为:{}",key);
DYNAMIC_DATASOURCE_KEY.set(key);
}
/**
* 获取动态数据源名称,默认使用mater数据源
*/
public static String getDynamicDataSourceKey(){
String key = DYNAMIC_DATASOURCE_KEY.get();
return key == null ? CommonConstant.MASTER : key;
}
/**
* 移除当前数据源
*/
public static void removeDynamicDataSourceKey(){
log.info("移除数据源:{}",DYNAMIC_DATASOURCE_KEY.get());
DYNAMIC_DATASOURCE_KEY.remove();
}
}
7、自定义注解
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource
{
/**
* 切换数据源名称
*/
public String value() default CommonConstant.MASTER;
}
8 aop切面
import com.alibaba.druid.pool.DruidDataSource;
import com.liubujun.config.*;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.*;
@Aspect
@Component
@Slf4j
public class DataSourceAspect {
// 设置DataSource注解的切点表达式
// @Pointcut("@annotation(com.liubujun.config.aespect.DataSource)")
@Pointcut("execution(public * com.liubujun.service..*.*(..))")
public void dynamicDataSourcePointCut(){
}
//环绕通知
@Around("dynamicDataSourcePointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
DataSource defineAnnotation = getDefineAnnotation(joinPoint);
String key = "";
//判断方法上是否有注解,没有注解则默认是走的是主服务器
if (defineAnnotation == null ) {
key = CommonConstant.MASTER;
}else {
key = defineAnnotation.value();
}
//判断数据库是否断开连接
key = getConnection(key);
DynamicDataSourceHolder.setDynamicDataSourceKey(key);
Object proceed = null;
try {
proceed = joinPoint.proceed();
} finally {
DynamicDataSourceHolder.removeDynamicDataSourceKey();
}
return proceed;
}
/**
* 先判断方法的注解,后判断类的注解,以方法的注解为准
* @param joinPoint
* @return
*/
private DataSource getDefineAnnotation(ProceedingJoinPoint joinPoint){
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
DataSource dataSourceAnnotation = methodSignature.getMethod().getAnnotation(DataSource.class);
if (Objects.nonNull(methodSignature)) {
return dataSourceAnnotation;
} else {
Class<?> dsClass = joinPoint.getTarget().getClass();
return dsClass.getAnnotation(DataSource.class);
}
}
/**
* 判断数据库是否连接成功
* @return
*/
private String getConnection(String target) throws SQLException {
//将数据源名称添加到list集合,方便后续操作
List<String> dataSources = new ArrayList<>();
dataSources.add(CommonConstant.SLAVE);
dataSources.add(CommonConstant.MASTER);
//获取装配好的bean对象
DruidConfig druidConfig = (DruidConfig)SpringUtil.getBean("druidConfig");
DruidDataSource druidDataSource = new DruidDataSource();
if (target.equals(CommonConstant.SLAVE)) {
druidDataSource = (DruidDataSource) druidConfig.slaveDataSource();
}
if (target.equals(CommonConstant.MASTER)) {
druidDataSource = (DruidDataSource) druidConfig.masterDataSource();
}
try {
Connection connection = DriverManager.getConnection(druidDataSource.getUrl(), druidDataSource.getUsername(), druidDataSource.getPassword());
} catch (SQLException e) {
dataSources.remove(target);
// shuffle 打乱顺序
Collections.shuffle(dataSources);
String changeTarget = dataSources.get(0);
getConnection(changeTarget);
log.info("========================数据源:{}连接异常,切换数据源为:{}===========================",target,changeTarget);
return changeTarget;
}
return target;
}
}
9 获取bean对象工具类
@Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if(SpringUtil.applicationContext == null) {
SpringUtil.applicationContext = applicationContext;
}
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
//根据类名获取指定对象
public static Object getBean(String name){
return getApplicationContext().getBean(name);
}
//根据类型获取指定对象
public static <T> T getBean(Class<T> clazz){
return getApplicationContext().getBean(clazz);
}
//根据类名和类型获取指定对象
public static <T> T getBean(String name,Class<T> clazz){
return getApplicationContext().getBean(name, clazz);
}
}
以上就是在代码层面动态切换数据源的相关代码,那么如何使用呢?
可以直接在业务service的实现层直接在方法上添加注解指定数据源,如:
我在这个方法上指定的是从数据库,如果此时从数据库发生宕机,那么就会自动切换到主数据库进行操作