SpringBoot+Jpa动态切换多数据源配置及实现
数据源配置文件:conf.properties
spring.datasource.primary.key=huitu spring.datasource.primary.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.primary.driverClassName=com.mysql.jdbc.Driver spring.datasource.primary.url=jdbc:mysql://15d451d6752.iok.la:3306/tuoying?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false spring.datasource.primary.username=root spring.datasource.primary.password=tuoying678 spring.datasource.secondary.key=dzzw spring.datasource.secondary.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.secondary.driverClassName=com.mysql.jdbc.Driver spring.datasource.secondary.url=jdbc:mysql://192.168.0.18:3306/dzzw?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false spring.datasource.secondary.username=root spring.datasource.secondary.password=123456 spring.datasource.skdd.key=skdd spring.datasource.skdd.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.skdd.driverClassName=com.mysql.jdbc.Driver spring.datasource.skdd.url=jdbc:mysql://192.168.0.18:3306/skdd?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false spring.datasource.skdd.username=root spring.datasource.skdd.password=123456
创建包prop,该包下放多数据源的注册类,配置类和自定义注解类
自定义注解类:DS
package com.codecat.portal.prop; import java.lang.annotation.*; /** * 1 * 自定义注解:DS */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) @Inherited public @interface DS { String value() default "huitu"; }
数据源信息类:DynamicDataSourceContextHolder
package com.codecat.portal.prop; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; /** * 创建存储数据源信息的类,并自定义实现AbstractRoutingDataSource */ public class DynamicDataSourceContextHolder { private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class); /* * 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本, * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 */ private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>(); /* * 管理所有的数据源id; * 主要是为了判断数据源是否存在; */ public static List<String> dataSourceIds = new ArrayList<String>(); //设置数据源 public static void setDataSourceType(String dataSourceType){ logger.info("切换至{}数据源", dataSourceType); contextHolder.set(dataSourceType); } //获取数据源 public static String getDataSourceType(){ return contextHolder.get(); } //清除数据源 public static void clearDataSourceType(){ contextHolder.remove(); } public static void saveDataSourceTypeName(String name){ dataSourceIds.add(name); } /** * 判断指定DataSrouce当前是否存在 * @param dataSourceId * @return */ public static boolean containsDataSource(String dataSourceId){ return dataSourceIds.contains(dataSourceId); } }
动态获取数据源类:DynamicDataSource,实现AbstractRoutingDataSource类,通过AOP切面拦截特定注解(DS)设定数据源,可以在Dao层或服务实现类中设置数据源注解(DS)
package com.codecat.portal.prop; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * 动态获取数据源 */ public class DynamicDataSource extends AbstractRoutingDataSource { private static final Logger logger = LoggerFactory.getLogger(DynamicDataSource.class); /* * 代码中的determineCurrentLookupKey方法取得一个字符串, * 该字符串将与配置文件中的相应字符串进行匹配以定位数据源,配置文件,即applicationContext.xml文件中需要要如下代码:(non-Javadoc) * @see org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineCurrentLookupKey() */ @Override protected Object determineCurrentLookupKey(){ /* * DynamicDataSourceContextHolder代码中使用setDataSourceType * 设置当前的数据源,在路由类中使用getDataSourceType进行获取, * 交给AbstractRoutingDataSource进行注入使用。 */ String dataSourceName = DynamicDataSourceContextHolder.getDataSourceType(); logger.info("当前数据源是:{}", dataSourceName); return dataSourceName; } }
多数据源注册类:DynamicDataSourceRegister
package com.codecat.portal.prop; import com.alibaba.druid.pool.DruidDataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertyNameAliases; import org.springframework.boot.context.properties.source.ConfigurationPropertySource; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.EnvironmentAware; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotationMetadata; import org.springframework.util.StringUtils; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * 动态注册数据源 * 启动动态数据源请在启动类中(如SpringBootSampleApplication) * 添加 @Import(DynamicDataSourceRegister.class) */ public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware { private DruidDataSource pool; private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceRegister.class); private ConversionService conversionService = new DefaultConversionService(); /** * 别名 */ private final static ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); /** * 由于部分数据源配置不同,所以在此处添加别名,避免切换数据源出现某些参数无法注入的情况 */ static { aliases.addAliases("url", new String[]{"jdbc-url"}); aliases.addAliases("username", new String[]{"user"}); } //存储注册的数据源 private Map<String, DataSource> customDataSources = new HashMap<>(); //配置上下文(也可以理解为配置文件的获取工具) private Environment evn; //参数绑定工具 springboot2.0新推出 private Binder binder; @Override public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry){ //获取所有数据源配置 Map config,defaultDataSourceProperties; defaultDataSourceProperties = binder.bind("spring.datasource.primary", Map.class).get(); //获取数据源类型 String typeStr = evn.getProperty("spring.datasource.primary.type"); //获取数据源类型 Class<? extends DataSource> clazz = getDataSourceType(typeStr); //绑定默认数据源参数,也就是主数据源 DataSource consumerDatasource, defaultDatasource = bind(clazz, defaultDataSourceProperties); DynamicDataSourceContextHolder.dataSourceIds.add("master"); logger.info("注册默认数据源成功"); //获取第二数据源配置secondary config = binder.bind("spring.datasource.secondary", Map.class).get(); //设置从数据源 clazz = getDataSourceType((String) config.get("type")); defaultDataSourceProperties = config; // 绑定参数 consumerDatasource = bind(clazz, defaultDataSourceProperties); // 获取数据源的key,以便通过该key可以定位到数据源 String key = config.get("key").toString(); customDataSources.put(key, consumerDatasource); // 数据源上下文,用于管理数据源与记录已经注册的数据源key DynamicDataSourceContextHolder.dataSourceIds.add(key); logger.info("注册数据源{}成功", key); //获取第三数据源配置skdd config = binder.bind("spring.datasource.skdd", Map.class).get(); //设置从数据源 clazz = getDataSourceType((String) config.get("type")); defaultDataSourceProperties = config; // 绑定参数 consumerDatasource = bind(clazz, defaultDataSourceProperties); // 获取数据源的key,以便通过该key可以定位到数据源 String skddKey = config.get("key").toString(); customDataSources.put(skddKey, consumerDatasource); // 数据源上下文,用于管理数据源与记录已经注册的数据源key DynamicDataSourceContextHolder.dataSourceIds.add(skddKey); logger.info("注册数据源{}成功", skddKey); // bean定义类 GenericBeanDefinition define = new GenericBeanDefinition(); // 设置bean的类型,此处DynamicRoutingDataSource是继承AbstractRoutingDataSource的实现类 define.setBeanClass(DynamicDataSource.class); // 需要注入的参数 MutablePropertyValues mpv = define.getPropertyValues(); // 添加默认数据源,避免key不存在的情况没有数据源可用 mpv.add("defaultTargetDataSource", defaultDatasource); // 添加其他数据源 mpv.add("targetDataSources", customDataSources); // 将该bean注册为datasource,不使用springboot自动生成的datasource beanDefinitionRegistry.registerBeanDefinition("datasource", define); logger.info("注册数据源成功,一共注册{}个数据源", customDataSources.keySet().size() + 1); } /** * 通过字符串获取数据源class对象 * * @param typeStr * @return */ private Class<? extends DataSource> getDataSourceType(String typeStr) { Class<? extends DataSource> type; try { if (StringUtils.hasLength(typeStr)) { // 字符串不为空则通过反射获取class对象 type = (Class<? extends DataSource>) Class.forName(typeStr); } else { // 默认为Druid数据源 type = DruidDataSource.class; } return type; } catch (Exception e) { throw new IllegalArgumentException("can not resolve class with type: " + typeStr); //无法通过反射获取class对象的情况则抛出异常,该情况一般是写错了,所以此次抛出一个runtimeexception } } /** * 绑定参数,以下三个方法都是参考DataSourceBuilder的bind方法实现的,目的是尽量保证我们自己添加的数据源构造过程与springboot保持一致 * * @param result * @param properties */ private void bind(DataSource result, Map properties) { ConfigurationPropertySource source = new MapConfigurationPropertySource(properties); Binder binder = new Binder(new ConfigurationPropertySource[]{source.withAliases(aliases)}); // 将参数绑定到对象 binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(result)); } private <T extends DataSource> T bind(Class<T> clazz, Map properties) { ConfigurationPropertySource source = new MapConfigurationPropertySource(properties); Binder binder = new Binder(new ConfigurationPropertySource[]{source.withAliases(aliases)}); // 通过类型绑定参数并获得实例对象 return binder.bind(ConfigurationPropertyName.EMPTY, Bindable.of(clazz)).get(); } /** * @param clazz * @param sourcePath 参数路径,对应配置文件中的值,如: spring.datasource * @param <T> * @return */ private <T extends DataSource> T bind(Class<T> clazz, String sourcePath) { Map properties = binder.bind(sourcePath, Map.class).get(); return bind(clazz, properties); } /** * EnvironmentAware接口的实现方法,通过aware的方式注入,此处是environment对象 * * @param environment */ @Override public void setEnvironment(Environment environment) { logger.info("开始注册数据源"); this.evn = environment; // 绑定配置器 binder = Binder.get(evn); } }
切换数据源类:DynamicDataSourceAspect
package com.codecat.portal.prop; 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.hibernate.engine.spi.SessionImplementor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; /** * 切换数据源Advice */ @Aspect @Order(-1)//设置AOP执行顺序(需要在事务之前,否则事务只发生在默认库中) @Component public class DynamicDataSourceAspect { @PersistenceContext private EntityManager entityManager; private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class); /* * @Before("@annotation(ds)") * 的意思是: * @Before:在方法执行之前进行执行:@annotation(targetDataSource): * 会拦截注解targetDataSource的方法,否则不拦截; */ @Before("@annotation(ds)") public void changeDataSource(JoinPoint point, DS ds) throws Throwable { //获取当前的指定的数据源; String dsId = ds.value(); /** * 通过aop拦截,获取注解上面的value的值key,然后取判断我们注册的keys集合中是否有这个key,如果没有,则使用默认数据源,如果有,则设置上下文中当前数据源的key为注解的value。 */ if (DynamicDataSourceContextHolder.containsDataSource(dsId)) { //logger.debug("切入:{} >", dsId, point.getSignature()); DynamicDataSourceContextHolder.setDataSourceType(dsId); } else { logger.info("数据源[{}]不存在,使用默认数据源 >{}", dsId, point.getSignature()); DynamicDataSourceContextHolder.setDataSourceType("master"); } } @After("@annotation(ds)") public void restoreDataSource(JoinPoint point, DS ds) { //logger.debug("Revert DataSource : {} > {}", ds.value(), point.getSignature()); //logger.info("切完"); //方法执行完毕之后,销毁当前数据源信息,进行垃圾回收。 DynamicDataSourceContextHolder.clearDataSourceType(); SessionImplementor session = entityManager.unwrap(SessionImplementor.class); //最关键的一句代码, 手动断开连接,不用重新设置 ,会自动重新设置连接。 session.disconnect(); } }
项目启动类添加代码(标黄代码):
package com.codecat; import com.codecat.portal.prop.DynamicDataSourceRegister; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletComponentScan; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.PropertySource; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.transaction.annotation.EnableTransactionManagement; import com.codecat.framework.jpa.EssenceJpaRepositoryFactoryBean; @EnableAutoConfiguration @ServletComponentScan @EnableTransactionManagement @ComponentScan(basePackages = {"com.URMS","com.codecat"}) @SpringBootApplication @Import({DynamicDataSourceRegister.class}) @EnableCaching //启用ehcach @EnableScheduling // 启用定时任务 @EnableJpaRepositories(repositoryFactoryBeanClass = EssenceJpaRepositoryFactoryBean.class) @PropertySource({ "classpath:druid.properties", "classpath:prop/${spring.profiles.active}/conf.properties", "classpath:application.properties" }) public class EbootApplication { static Logger logger = LoggerFactory.getLogger(EbootApplication.class); public static void main(String[] args) { SpringApplication application = new SpringApplication(EbootApplication.class); application.setBannerMode(Banner.Mode.OFF); application.run(args); logger.info("spring cloud start success"); } }
服务实现类中使用DS注解来标明使用哪一个数据源,主数据源不用添加(第二,三数据源需要加)
package com.codecat.portal.zw.service.impl; import com.codecat.framework.jpa.Criterion; import com.codecat.framework.jpa.Paginator; import com.codecat.framework.jpa.PaginatorParam; import com.codecat.portal.prop.DS; import com.codecat.portal.zw.dao.YwNoticeDao; import com.codecat.portal.zw.entity.YwNotice; import com.codecat.portal.zw.service.ZwService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.text.SimpleDateFormat; import java.util.*; /** * 政务信息 - 服务接口实现 */ @Service public class ZwServiceImpl implements ZwService { @Autowired YwNoticeDao ywNoticeDao; /** * 按时间倒序查询前8条资讯和通知 * @return */ @Override @DS("gx_dzzw") public List<Map<String,Object>> queryNoticeTop8(){ List<Map<String,Object>> mapList = new ArrayList<>(); //... return mapList; } /** * 根据ID查询一个资讯内容 * @param id * @return */ @Override @DS("gx_dzzw") public YwNotice queryNoticeDetailById(String id){ YwNotice ywNotice = ywNoticeDao.queryDetailById(id); return ywNotice; } /** * 分页获取信息发布记录 * @param param 条件 * @return 分页结果 */ @Override @DS("gx_dzzw") public Paginator<YwNotice> queryNoticeListPage(PaginatorParam param){ Paginator<YwNotice> all = ywNoticeDao.findAll(param); //... return all; } }