數據源配置文件: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; } }