项目背景:最近公司中需要搭建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: 8088spring:datasource:druid:type: com.alibaba.druid.pool.DruidDataSourcemaster:url: jdbc:mysql://192.168.26.4:3306/test01?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghaiusername: rootpassword: rootdriver-class-name: com.mysql.jdbc.Driverslave:url: jdbc:mysql://192.168.26.8:3306/test01?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghaiusername: rootpassword: rootdriver-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@Primarypublic 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;/*** 决定当前线程使用哪个数据源*/@Overrideprotected 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;@Overridepublic 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的实现层直接在方法上添加注解指定数据源,如:
我在这个方法上指定的是从数据库,如果此时从数据库发生宕机,那么就会自动切换到主数据库进行操作