2.application.yaml
spring: freemarker: cache: false charset: utf-8 enabled: true datasource: type: com.alibaba.druid.pool.DruidDataSource primary: driverClassName: com.mysql.jdbc.Driver username: root password: root url: jdbc:mysql://localhost:3306/as type: com.alibaba.druid.pool.DruidDataSource local: driver-class-name: com.mysql.jdbc.Driver username: root password: root url: jdbc:mysql://localhost:3306/test type: com.alibaba.druid.pool.DruidDataSource prod: driver-class-name: com.mysql.jdbc.Driver username: root password: root url: jdbc:mysql://localhost:3306/guns type: com.alibaba.druid.pool.DruidDataSource server: port: 8085 diy.user: id: 12 logging: file: /log.txt level: trace mybatis-plus: mapper-locations: classpath:mapper/*Mapper.xml typeAliasesPackage: global-config: id-type: 3 refresh-mapper: true capital-mode: true field-strategy: 2 db-column-underline: false
3.configuration
package com.river.datasource; import com.alibaba.druid.pool.DruidDataSource; import com.river.common.ContextConst; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; import java.util.HashMap; @Configuration public class MutiplyDataSource { @Bean(name = "dataSourcePrimary") @ConfigurationProperties(prefix = "spring.datasource.primary") public DataSource primaryDataSource(){ return new DruidDataSource(); } @Bean(name = "dataSourceLocal") @ConfigurationProperties(prefix = "spring.datasource.local") public DataSource localDataSource(){ return new DruidDataSource(); } @Bean(name = "dataSourceProd") @ConfigurationProperties(prefix = "spring.datasource.prod") public DataSource prodDataSource(){ return new DruidDataSource(); } @Primary @Bean(name = "dynamicDataSource") public DataSource dynamicDataSource() { DynamicDataSource dynamicDataSource = new DynamicDataSource(); //配置默认数据源 dynamicDataSource.setDefaultTargetDataSource(primaryDataSource()); //配置多数据源 HashMap<Object, Object> dataSourceMap = new HashMap(); dataSourceMap.put(ContextConst.DataSourceType.PRIMARY.name(),primaryDataSource()); dataSourceMap.put(ContextConst.DataSourceType.LOCAL.name(),localDataSource()); dataSourceMap.put(ContextConst.DataSourceType.PROD.name(),prodDataSource()); dynamicDataSource.setTargetDataSources(dataSourceMap); return dynamicDataSource; } /** * 配置@Transactional注解事务 * @return */ @Bean public PlatformTransactionManager transactionManager() { return new DataSourceTransactionManager(dynamicDataSource()); } }
4.数据源持有类
package com.river.datasource; import lombok.extern.log4j.Log4j; @Log4j public class DataSourceContextHolder { private static final String DEFAULT_DATASOURCE = "PRIMARY_DATASOURCE"; private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>(); public static void setDataSource(String dbType){ log.info("切换到["+dbType+"]数据源"); contextHolder.set(dbType); } public static String getDataSource(){ return contextHolder.get(); } public static void clearDataSource(){ contextHolder.remove(); } }
5.定义切换数据源的注解和切面
package com.river.annotation; import com.river.common.ContextConst; import java.lang.annotation.*; @Target({ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataSource { ContextConst.DataSourceType value() default ContextConst.DataSourceType.PRIMARY; }
package com.river.aspect; import com.river.annotation.DataSource; import com.river.common.ContextConst; import com.river.datasource.DataSourceContextHolder; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import java.lang.reflect.Method; @Component @Aspect public class DynamicDataSourceAspect { @Before("execution(* com.river.service..*.*(..))") public void before(JoinPoint point){ try { DataSource annotationOfClass = point.getTarget().getClass().getAnnotation(DataSource.class); String methodName = point.getSignature().getName(); Class[] parameterTypes = ((MethodSignature) point.getSignature()).getParameterTypes(); Method method = point.getTarget().getClass().getMethod(methodName, parameterTypes); DataSource methodAnnotation = method.getAnnotation(DataSource.class); methodAnnotation = methodAnnotation == null ? annotationOfClass:methodAnnotation; ContextConst.DataSourceType dataSourceType = methodAnnotation != null && methodAnnotation.value() !=null ? methodAnnotation.value() :ContextConst.DataSourceType.PRIMARY ; DataSourceContextHolder.setDataSource(dataSourceType.name()); } catch (NoSuchMethodException e) { e.printStackTrace(); } } @After("execution(* com.river.service..*.*(..))") public void after(JoinPoint point){ DataSourceContextHolder.clearDataSource(); } }
package com.river.common; public interface ContextConst { enum DataSourceType{ PRIMARY,LOCAL,PROD,TEST } }
6.数据源路由实现类
package com.river.datasource; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; public class DynamicDataSource extends AbstractRoutingDataSource{ @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSource(); } }
7.使用时,通过注解指定数据源
package com.river.service.impl; import com.baomidou.mybatisplus.service.impl.ServiceImpl; import com.river.annotation.DataSource; import com.river.common.ContextConst; import com.river.entity.User; import com.river.mapper.PrimaryUserMapper; import com.river.service.ParmaryUserService; import lombok.Data; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class ParmaryUserServiceImpl extends ServiceImpl<PrimaryUserMapper,User> implements ParmaryUserService{ @Autowired private PrimaryUserMapper primaryUserMapper; @Override public List<User> sel(){ return primaryUserMapper.selectList(null); } @DataSource(ContextConst.DataSourceType.PROD) @Override public List<User> sell() { return primaryUserMapper.selectList(null); } @DataSource(ContextConst.DataSourceType.LOCAL) @Override public List<User> selle() { return primaryUserMapper.selectList(null); } }
7.另外,补充一下其他部分代码;
entity:
@Data @TableName("ACT_USER") public class User { @TableId @TableField private Integer id; @TableField("USERNAME") private String username; @TableField("PASSWORD") private String password; @TableField("ROLE_ID") private Integer roleId; }
mapper:
package com.river.mapper; import com.baomidou.mybatisplus.mapper.BaseMapper; import com.river.entity.User; public interface PrimaryUserMapper extends BaseMapper<User>{ }
controller:
@RestController public class DataController { @Autowired private ParmaryUserService parmaryUserService; @RequestMapping("login") public List<User> login(Integer type){ switch (type){ case 1: return parmaryUserService.sel(); case 2: return parmaryUserService.sell(); default: return parmaryUserService.selle(); } } }
入口类:
package com.river; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.BeansException; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @MapperScan("com.river.mapper") //排除DataSource自动配置类,否则会默认自动配置,不会使用我们自定义的DataSource,并且启动报错 @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) public class GoalintlApplication implements CommandLineRunner,ApplicationContextAware{ public static void main(String[] args) { SpringApplicationBuilder springApplicationBuilder = new SpringApplicationBuilder(GoalintlApplication.class); springApplicationBuilder.profiles("dev").logStartupInfo(true).run(args); } @Override public void run(String... args) throws Exception { } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { } }
说明一下实现思路:
springboot有提供AbstractRoutingDataSource#determineCurrentLookupKey抽象方法去指定数据源,我们要做的就是实现切换数据源的逻辑;通过AOP在调用数据库之前切换数据源;
本来在切面内做了一个缓存,记录上一次使用的数据源,如果这一次使用相同的就不用切换了,但是发现初始化数据连接才是消耗大的,后面切换数据源其实就是去指定用哪个数据库连接而已,不再消耗资源了;
下面的代码显示了切换数据源时只是通过key去拿对应的dataSource,而相关的dataSource在第一次调用时就初始化一次就可以了;
protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } return dataSource; }