项目背景:最近公司中需要搭建mysql的主从,想着在spring中集成多数据源。mybatisplus提供的有插件用@DS注解就能够实现,但是这种在mysql服务宕机的情况下不能够进行自动切换,于是就想着用aop+自定义注解的方式来实现
项目实现效果:如果公司服务器搭建的是一主多从多个mysql数据源,主服务器用来读。从服务器用来写。此时你在代码层面用注解指定了一个增删改方法到从数据源,但是碰巧此时从数据源失效了,那么就会自动的切换到其它服务器。代码实现如下:
注意:为了节省篇幅,向controller、service层就不展示出来了,只展示相关核心代码。
1、pom文件
org.springframework.boot spring-boot-starter-weborg.springframework.boot spring-boot-starter-aoporg.springframework.boot spring-boot-starter-jdbcorg.projectlombok lomboktrue org.springframework.boot spring-boot-starter-testtest com.alibaba druid-spring-boot-starter1.2.8 com.baomidou dynamic-datasource-spring-boot-starter2.4.2 mysql mysql-connector-java8.0.21 com.baomidou mybatis-plus-boot-starter3.2.0 com.baomidou mybatis-plus-generator3.3.2 commons-lang commons-lang2.6 org.apache.velocity velocity-engine-core2.0 log4j log4j1.2.14
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
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
6、DynamicDataSourceHolder
DynamicDataSourceHolder类主要是设置当前线程的数据源名称,移除数据源名称,以及获取当前数据源的名称,便于动态切换
/** * 数据源切换处理 * * @author zhangyu */ @Slf4j public class DynamicDataSourceHolder { /** * 保存动态数据源名称 */ private static final ThreadLocalDYNAMIC_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集合,方便后续操作 ListdataSources = 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 staticT getBean(Class clazz){ return getApplicationContext().getBean(clazz); } //根据类名和类型获取指定对象 public static T getBean(String name,Class clazz){ return getApplicationContext().getBean(name, clazz); } }
以上就是在代码层面动态切换数据源的相关代码,那么如何使用呢?
可以直接在业务service的实现层直接在方法上添加注解指定数据源,如:
我在这个方法上指定的是从数据库,如果此时从数据库发生宕机,那么就会自动切换到主数据库进行操作