參考文章:https://www.cnblogs.com/hehehaha/p/6147096.htmlhtml
前言java
目標是springboot工程支持多個MySQL數據源,在代碼層面上,同一個SQL(Mapper)能夠在多個數據源靈活使用,也就是所說的動態。mysql
這種動態是經過LocalThread實現的,即一個web請求對應一個線程,在線程中指定一個數據源。web
一、maven pomspring
pom.xml裏有springboot的starter和數據庫驅動,我這裏用的是druidsql
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.ivan.studio</groupId> <artifactId>fantacy</artifactId> <version>1.0-SNAPSHOT</version> <properties> <!-- 文件拷貝時的編碼 --> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <!-- 編譯時的編碼 --> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> <jdk.version>1.7</jdk.version> <hive.version>1.2.1</hive.version> <hadoop.version>2.7.2</hadoop.version> <shiro.version>1.4.0</shiro.version> </properties> <!-- 繼承父包 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.3.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.3.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.1</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>1.4.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>1.4.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <version>1.4.3.RELEASE</version> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>1.4.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>1.4.3.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <version>1.4.3.RELEASE</version> </dependency> <!--<dependency>--> <!--<groupId>org.apache.tomcat</groupId>--> <!--<artifactId>tomcat-jdbc</artifactId>--> <!--</dependency>--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.20</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>4.3.8.RELEASE</version> </dependency> </dependency> <!-- druid阿里巴巴數據庫鏈接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.15</version> </dependency> </dependencies> </project>
二、配置文件數據庫
application.properties配置文件裏要有主數據源,多數據源,線程池等配置apache
# 主數據源 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://127.0.0.1:3306/ivan?useUnicode=true&characterEncoding=utf-8 spring.datasource.username=root spring.datasource.password=123456 # 更多數據源 custom.datasource.names=ds1,ds2 custom.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource custom.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver custom.datasource.ds1.url=jdbc:mysql://127.0.0.1:3306/vicky?useUnicode=true&characterEncoding=utf-8 custom.datasource.ds1.username=root custom.datasource.ds1.password=123456 custom.datasource.ds2.type=com.alibaba.druid.pool.DruidDataSource custom.datasource.ds2.driver-class-name=com.mysql.jdbc.Driver custom.datasource.ds2.url=jdbc:mysql://127.0.0.1:3306/ivan?useUnicode=true&characterEncoding=utf-8 custom.datasource.ds2.username=root custom.datasource.ds2.password=123456 #鏈接池的配置信息 spring.datasource.initialSize=10 spring.datasource.minIdle=10 spring.datasource.maxActive=100 spring.datasource.maxWait=60000 spring.datasource.timeBetweenEvictionRunsMillis=60000 spring.datasource.minEvictableIdleTimeMillis=300000 spring.datasource.validationQuery=SELECT 1 spring.datasource.testWhileIdle=true spring.datasource.testOnBorrow=false spring.datasource.testOnReturn=false spring.datasource.poolPreparedStatements=true spring.datasource.maxPoolPreparedStatementPerConnectionSize=20 spring.datasource.filters=stat,wall,log4j spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
三、動態數據源路由類tomcat
動態數據源能進行自動切換的核心就是spring底層提供了AbstractRoutingDataSource類進行數據源的路由的,咱們主要繼承這個類,實現裏面的方法便可實現咱們想要的,這裏主要是實現方法:determineCurrentLookupKey(),而此方法只須要返回一個數據庫的名稱便可,因此咱們核心的是有一個類來管理數據源的線程池,這個類纔是動態數據源的核心處理類。還有另外就是咱們使用aop技術在執行事務方法前進行數據源的切換。因此這裏有幾個須要編碼的類,具體以下:springboot
import java.util.ArrayList; import java.util.List; /** * @Author: ivan * @Description: * @Date: Created in 15:23 18/5/24 * @Modified By: */ public class DynamicDataSourceContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>(); public static List<String> dataSourceIds = new ArrayList<String>(); public static void setDataSourceType(String dataSourceType) { contextHolder.set(dataSourceType); } public static String getDataSourceType() { return contextHolder.get(); } public static void clearDataSourceType() { contextHolder.remove(); } /** * 判斷指定DataSrouce當前是否存在 * * @param dataSourceId * @return * @author ivan * @create 2016年1月24日 */ public static boolean containsDataSource(String dataSourceId){ return dataSourceIds.contains(dataSourceId); } }
數據源路由類:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * 動態多數據源 * @Author: ivan * @Description: * @Date: Created in 15:22 18/5/24 * @Modified By: */ public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceContextHolder.getDataSourceType(); } }
指定數據源註解類>com.kfit.config.datasource.dynamic.TargetDataSource:
import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @Author: ivan * @Description: * @Date: Created in 15:29 18/5/24 * @Modified By: */ @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface TargetDataSource { String name(); }
切換數據源Advice:
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.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; /** * @Author: ivan * @Description: * @Date: Created in 15:25 18/5/24 * @Modified By: */ /** * 切換數據源Advice * * @author ivan */ @Aspect @Order(-10)// 保證該AOP在@Transactional以前執行 @Component public class DynamicDataSourceAspect { private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class); @Before("@annotation(ds)") public void changeDataSource(JoinPoint point, TargetDataSource ds) throws Throwable { String dsId = ds.name(); if (!DynamicDataSourceContextHolder.containsDataSource(dsId)) { logger.error("ds=[{}]not exist,use main DataSource > {}", ds.name(), point.getSignature()); } else { logger.debug("Use DataSource : {} > {}", ds.name(), point.getSignature()); DynamicDataSourceContextHolder.setDataSourceType(ds.name()); } } @After("@annotation(ds)") public void restoreDataSource(JoinPoint point, TargetDataSource ds) { logger.debug("Revert DataSource : {} > {}", ds.name(), point.getSignature()); DynamicDataSourceContextHolder.clearDataSourceType(); } }
四、註冊多數據源
以上都是動態數據源在注入的時候使用的代碼,其實很重要的一部分代碼就是註冊咱們在application.properties配置的多數據源,這纔是重點,這裏咱們使用
ImportBeanDefinitionRegistrar進行註冊,具體的代碼以下:
package com.ivan.studio.fantacy.common; /** * @Author: ivan * @Description: * @Date: Created in 15:27 18/5/24 * @Modified By: */ import com.alibaba.druid.pool.DruidDataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; import org.springframework.boot.bind.RelaxedDataBinder; import org.springframework.boot.bind.RelaxedPropertyResolver; 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.core.env.Environment; import org.springframework.core.type.AnnotationMetadata; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * 動態數據源註冊<br/> * 啓動動態數據源請在啓動類中(如SpringBootSampleApplication) * 添加 @Import(DynamicDataSourceRegister.class) * * @author ivan */ public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware { private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceRegister.class); private ConversionService conversionService = new DefaultConversionService(); private PropertyValues dataSourcePropertyValues; // 如配置文件中未指定數據源類型,使用該默認值 private static final Object DATASOURCE_TYPE_DEFAULT = "org.apache.tomcat.jdbc.pool.DataSource"; // private static final Object DATASOURCE_TYPE_DEFAULT = // "com.zaxxer.hikari.HikariDataSource"; // 數據源 private DataSource defaultDataSource; private Map<String, DataSource> customDataSources = new HashMap<String, DataSource>(); @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { Map<Object, Object> targetDataSources = new HashMap<Object, Object>(); // 將主數據源添加到更多數據源中 targetDataSources.put("dataSource", defaultDataSource); DynamicDataSourceContextHolder.dataSourceIds.add("dataSource"); // 添加更多數據源 targetDataSources.putAll(customDataSources); for (String key : customDataSources.keySet()) { DynamicDataSourceContextHolder.dataSourceIds.add(key); } // 建立DynamicDataSource GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); beanDefinition.setBeanClass(DynamicDataSource.class); beanDefinition.setSynthetic(true); MutablePropertyValues mpv = beanDefinition.getPropertyValues(); mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource); mpv.addPropertyValue("targetDataSources", targetDataSources); registry.registerBeanDefinition("dataSource", beanDefinition); logger.info("Dynamic DataSource Registry"); } /** * 建立DataSource * @return * @author ivan * @create 2016年1月24日 */ @SuppressWarnings("unchecked") public DataSource buildDataSource(Map<String, Object> dsMap) { try { Object type = dsMap.get("type"); if (type == null) { type = DATASOURCE_TYPE_DEFAULT;// 默認DataSource } Class<? extends DataSource> dataSourceType; dataSourceType = (Class<? extends DataSource>) Class.forName((String) type); String driverClassName = dsMap.get("driver-class-name").toString(); String url = dsMap.get("url").toString(); String username = dsMap.get("username").toString(); String password = dsMap.get("password").toString(); DataSourceBuilder factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url) .username(username).password(password).type(dataSourceType); return factory.build(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return null; } public DataSource getDataSource(Map<String, Object> dsMap) { DruidDataSource datasource = new DruidDataSource(); datasource.setUrl(dsMap.get("url").toString()); datasource.setUsername(dsMap.get("username").toString()); datasource.setPassword(dsMap.get("password").toString()); datasource.setDriverClassName(dsMap.get("driver-class-name").toString()); return datasource; } /** * 加載多數據源配置 */ @Override public void setEnvironment(Environment env) { initDefaultDataSource(env); initCustomDataSources(env); } /** * 初始化主數據源 * * @author ivan */ private void initDefaultDataSource(Environment env) { // 讀取主數據源 RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(env, "spring.datasource."); Map<String, Object> dsMap = new HashMap<String, Object>(); dsMap.put("type", propertyResolver.getProperty("type")); dsMap.put("driver-class-name", propertyResolver.getProperty("driver-class-name")); dsMap.put("url", propertyResolver.getProperty("url")); dsMap.put("username", propertyResolver.getProperty("username")); dsMap.put("password", propertyResolver.getProperty("password")); defaultDataSource = getDataSource(dsMap); dataBinder(defaultDataSource, env); } /** * 爲DataSource綁定更多數據 * * @param dataSource * @param env * @author ivan */ private void dataBinder(DataSource dataSource, Environment env){ RelaxedDataBinder dataBinder = new RelaxedDataBinder(dataSource); //dataBinder.setValidator(new LocalValidatorFactory().run(this.applicationContext)); dataBinder.setConversionService(conversionService); dataBinder.setIgnoreNestedProperties(false);//false dataBinder.setIgnoreInvalidFields(false);//false dataBinder.setIgnoreUnknownFields(true);//true if(dataSourcePropertyValues == null){ Map<String, Object> rpr = new RelaxedPropertyResolver(env, "spring.datasource").getSubProperties("."); Map<String, Object> values = new HashMap<String, Object>(rpr); // 排除已經設置的屬性 values.remove("type"); values.remove("driver-class-name"); values.remove("url"); values.remove("username"); values.remove("password"); dataSourcePropertyValues = new MutablePropertyValues(values); } dataBinder.bind(dataSourcePropertyValues); } /** * 初始化更多數據源 * * @author ivan * @create 2016年1月24日 */ private void initCustomDataSources(Environment env) { // 讀取配置文件獲取更多數據源,也能夠經過defaultDataSource讀取數據庫獲取更多數據源 RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(env, "custom.datasource."); String dsPrefixs = propertyResolver.getProperty("names"); for (String dsPrefix : dsPrefixs.split(",")) { Map<String, Object> dsMap = propertyResolver.getSubProperties(dsPrefix + "."); DataSource ds = getDataSource(dsMap); customDataSources.put(dsPrefix, ds); dataBinder(ds, env); } } }
這裏還有一個步驟很重要,因爲咱們是使用的ImportBeanDefinitionRegistrar的方式進行註冊的,因此咱們須要在App.java類中使用@Import進行註冊,具體改造以後的App.java代碼以下:
import com.ivan.studio.fantacy.common.DynamicDataSourceRegister; import com.ivan.studio.fantacy.service.AutowiredService; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Import; import org.springframework.transaction.annotation.EnableTransactionManagement; import static org.springframework.boot.SpringApplication.run; /** * @author ivan */ @SpringBootApplication @Import({DynamicDataSourceRegister.class}) public class WebApplication { private static volatile boolean IS_REGISTRY = false; public static void main(String[] args) { SpringApplication.run(App.class, args); } }