先贴一下用的依赖项。
<!-- druid 数据库连接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.20</version> </dependency> <!-- mysql 驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- mybatis-plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- 多数据源--> <dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.5.2</version> </dependency> <!-- 上面是必须的 --> <!--spring-boot-web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 代码生成器插件--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.3.1</version> </dependency> <!-- 模版引擎 --> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> </dependency> <!-- lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!--huttol工具类--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.11</version> </dependency>
application.yaml
yaml配置文件的配置。
# 数据源配置 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver druid: # ps # 主库数据源 master: url: jdbc:mysql://localhost:3306/jzm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: 123456 # 从库数据源 slave: # 从数据源开关/默认关闭 enabled: false url: username: password: # 初始连接数 initialSize: 5 # 最小连接池数量 minIdle: 10 # 最大连接池数量 maxActive: 20 # 配置获取连接等待超时的时间 maxWait: 60000 # 配置连接超时时间 connectTimeout: 30000 # 配置网络超时时间 socketTimeout: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 minEvictableIdleTimeMillis: 300000 # 配置一个连接在池中最大生存的时间,单位是毫秒 maxEvictableIdleTimeMillis: 900000 # 配置检测连接是否有效 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false webStatFilter: enabled: true statViewServlet: enabled: true # 设置白名单,不填则允许所有访问 allow: url-pattern: /druid/* # 控制台管理用户名和密码 login-username: ruoyi login-password: 123456 filter: stat: enabled: true # 慢SQL记录 log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true
DruidConfig
负责读取yaml文件的数据源配置,生成数据源。还有创建动态数据源容器。另外ServletRegistrationBean 和 FilterRegistrationBean的配置和生成。
/** * druid 配置多数据源 * * @author jzm */ @Configuration public class DruidConfig { // 读取主数据源配置 @Bean @ConfigurationProperties("spring.datasource.druid.master") public DataSource masterDataSource(DruidProperties druidProperties) { DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); return druidProperties.dataSource(dataSource); } // 读取从数据源配置(默认关闭) @Bean @ConfigurationProperties("spring.datasource.druid.slave") @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true") public DataSource slaveDataSource(DruidProperties druidProperties) { DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); return druidProperties.dataSource(dataSource); } // 创建动态数据源容器 @Bean(name = "dynamicDataSource") @Primary public DynamicDataSource dataSource(DataSource masterDataSource) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource); setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource"); return new DynamicDataSource(masterDataSource, targetDataSources); } /** * 设置数据源 * * @param targetDataSources 备选数据源集合 * @param sourceName 数据源名称 * @param beanName bean名称 */ public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName) { try { DataSource dataSource = SpringUtil.getBean(beanName); // 从spring容器获得,bean实例 targetDataSources.put(sourceName, dataSource); } catch (Exception e) { } } /** * 去除监控页面底部的广告 */ @SuppressWarnings({"rawtypes", "unchecked"}) @Bean @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true") // 只有当这个配置为true是,这个bean才会被创建 public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) { // 获取web监控页面的参数 DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); // 提取common.js的配置路径 String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; String commonJsPattern = pattern.replaceAll("\*", "js/common.js"); final String filePath = "support/http/resources/js/common.js"; // 创建filter进行过滤 Filter filter = new Filter() { @Override public void init(javax.servlet.FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { chain.doFilter(request, response); // 重置缓冲区,响应头不会被重置 response.resetBuffer(); // 获取common.js String text = Utils.readFromResource(filePath); // 正则替换banner, 除去底部的广告信息 text = text.replaceAll("<a.*?banner"></a><br/>", ""); text = text.replaceAll("powered.*?shrek.wang</a>", ""); response.getWriter().write(text); } @Override public void destroy() { } }; FilterRegistrationBean registrationBean = new FilterRegistrationBean(); registrationBean.setFilter(filter); registrationBean.addUrlPatterns(commonJsPattern); return registrationBean; } /** * @description 注册一个StatViewServlet,进行druid监控页面配置 * @return servlet registration bean */ @Bean public ServletRegistrationBean druidStatViewServlet() { //先配置管理后台的servLet,访问的入口为/druid/ ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean( new StatViewServlet(), "/druid/*"); // IP白名单 (没有配置或者为空,则允许所有访问) servletRegistrationBean.addInitParameter("allow", "127.0.0.1"); // IP黑名单 (存在共同时,deny优先于allow) servletRegistrationBean.addInitParameter("deny", ""); servletRegistrationBean.addInitParameter("loginUsername", "admin"); servletRegistrationBean.addInitParameter("loginPassword", "123456"); servletRegistrationBean.addInitParameter("resetEnable", "false"); return servletRegistrationBean; } }
DynamicDataSource
就是我们的动态数据源,负责继承和初始化 AbstractRoutingDataSource。还有就是重写determineCurrentLookupKey()这个方法,让druid知道我们当前线程要使用那个数据源从而切换那个数据源。
/** * 动态数据源 * * @author jzm */ public class DynamicDataSource extends AbstractRoutingDataSource { public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) { super.setDefaultTargetDataSource(defaultTargetDataSource); super.setTargetDataSources(targetDataSources); super.afterPropertiesSet(); } /** * 获取当前线程正在用的数据源 */ @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceContextHolder.getDataSourceType(); } public static void setDataSource(String dataSource){ DynamicDataSourceContextHolder.setDataSourceType(dataSource); } public static void clearDataSource(){ DynamicDataSourceContextHolder.clearDataSourceType(); } }
DynamicDataSourceContextHolder
动态数据源上下文容器。
/** * 数据源切换处理 * * @author jzm */ public class DynamicDataSourceContextHolder { public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class); /** * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本, * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 */ private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); /** * 设置数据源的变量 */ public static void setDataSourceType(String dsType) { log.info("切换到{}数据源", dsType); CONTEXT_HOLDER.set(dsType); } /** * 获得数据源的变量 */ public static String getDataSourceType() { return CONTEXT_HOLDER.get(); } /** * 清空数据源变量 */ public static void clearDataSourceType() { CONTEXT_HOLDER.remove(); } }
DataSourceType
数据源类型。
/** * 数据源 * * @author jzm */ public enum DataSourceType { /** * 主库 */ MASTER, /** * 从库 */ SLAVE }
DruidProperties
读取我们yml配置文件的类。
/** * druid 数据源配置读取类 * * @author jzm */ @Configuration public class DruidProperties { @Value("${spring.datasource.druid.initialSize}") private int initialSize; @Value("${spring.datasource.druid.minIdle}") private int minIdle; @Value("${spring.datasource.druid.maxActive}") private int maxActive; @Value("${spring.datasource.druid.maxWait}") private int maxWait; @Value("${spring.datasource.druid.connectTimeout}") private int connectTimeout; @Value("${spring.datasource.druid.socketTimeout}") private int socketTimeout; @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}") private int timeBetweenEvictionRunsMillis; @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}") private int minEvictableIdleTimeMillis; @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}") private int maxEvictableIdleTimeMillis; @Value("${spring.datasource.druid.validationQuery}") private String validationQuery; @Value("${spring.datasource.druid.testWhileIdle}") private boolean testWhileIdle; @Value("${spring.datasource.druid.testOnBorrow}") private boolean testOnBorrow; @Value("${spring.datasource.druid.testOnReturn}") private boolean testOnReturn; public DruidDataSource dataSource(DruidDataSource datasource) { /** 配置初始化大小、最小、最大 */ datasource.setInitialSize(initialSize); datasource.setMaxActive(maxActive); datasource.setMinIdle(minIdle); /** 配置获取连接等待超时的时间 */ datasource.setMaxWait(maxWait); /** 配置驱动连接超时时间,检测数据库建立连接的超时时间,单位是毫秒 */ datasource.setConnectTimeout(connectTimeout); /** 配置网络超时时间,等待数据库操作完成的网络超时时间,单位是毫秒 */ datasource.setSocketTimeout(socketTimeout); /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */ datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */ datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis); /** * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 */ datasource.setValidationQuery(validationQuery); /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */ datasource.setTestWhileIdle(testWhileIdle); /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ datasource.setTestOnBorrow(testOnBorrow); /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ datasource.setTestOnReturn(testOnReturn); return datasource; } }
DataSource
我们自定义一个注解,标记我们当前注解的方法那个数据源。
/** * 自定义多数据源切换注解 * * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准 * * @author jzm */ @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface DataSource { /** * 切换数据源名称 */ DataSourceType value() default DataSourceType.MASTER; }
DataSourceAsept
我们定义一个切面类,负责获取我们的自定义数据源注解的值,从而在DynamicDataSource中的setDataSource() 方法,设置当前线程用的数据源。然后被determineCurrentLookupKey()方法获取到,从而切换数据源。注意在我们处理中方法的优先级比类的优先级高。
/** * 数据源切面类 * * @author jzm */ @Aspect @Component public class DataSourceAspect implements Ordered { Logger log = LoggerFactory.getLogger(DataSourceAspect.class); @Pointcut("@annotation(com.example.annotation.DataSource)") public void dataSourcePointCut() { } @Around("dataSourcePointCut()") public Object around(ProceedingJoinPoint point) throws Throwable { MethodSignature signature = (MethodSignature) point.getSignature(); // 处理方法 Method method = signature.getMethod(); // 处理类 Class<?> methodCls = method.getDeclaringClass(); DataSource clsDs = methodCls.getAnnotation(DataSource.class); DataSource meDs = method.getAnnotation(DataSource.class); if (meDs == null ) { if(clsDs == null){ log.debug("set datasource is " + DataSourceType.MASTER); }else{ DynamicDataSource.setDataSource(clsDs.value().name()); log.debug("set datasource is" + clsDs.value().name()); } } else { DynamicDataSource.setDataSource(meDs.value().name()); log.debug("set datasource is " + meDs.value().name()); } try { return point.proceed(); } finally { DynamicDataSource.clearDataSource(); log.debug("clean datasource"); } } @Override public int getOrder() { return 1; } }
看到这,是已经把配置都能完了。
测试
我们随便找一张表负责数据库连接测试。
生成对应的service、mapper、controller文件。
这里就展示service实现类的写法。
我们在对应的service实现里面。对想要切换数据源的方法标记@DataSource注解
@Service @DataSource(value = DataSourceType.MASTER) public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService { @Override @DataSource(value = DataSourceType.MASTER) public List<SysUser> findUserList() { return this.baseMapper.selectList(null); } }
然后,启动项目。
访问sql监控页面
http://127.0.0.1:8080/druid/index.html PS: ip和端口替换成自己的。
会出现一个登录页面。我们输入在DruidConfig中,配置好的用户名和密码。
@Bean public ServletRegistrationBean druidStatViewServlet() { //先配置管理后台的servLet,访问的入口为/druid/ ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean( new StatViewServlet(), "/druid/*"); // IP白名单 (没有配置或者为空,则允许所有访问) servletRegistrationBean.addInitParameter("allow", "127.0.0.1"); // IP黑名单 (存在共同时,deny优先于allow) servletRegistrationBean.addInitParameter("deny", ""); servletRegistrationBean.addInitParameter("loginUsername", "admin"); servletRegistrationBean.addInitParameter("loginPassword", "123456"); servletRegistrationBean.addInitParameter("resetEnable", "false"); return servletRegistrationBean; }
登录完毕,出现如下页面,代表我们druid监控配置是没问题的。
但是,这时候由于项目并没有crud操作,因此数据源后面sql监控信息都是空的。
利用postman进行测试。
再打开数据源页面,出现如下所示,代表成功了。
多数据源配置验证
我们在进行curd操作时,嗯出现如下,代表数据源切换应该成功。