spring boot 整合 druid 多数据源切换与druid 监控

该代码已运用正式项目中此处为测试demo,我自己理解得也不深但功能实现了,不足点还望大神指出

不足点,未能实现两个数据源之间的事务,只能实现切换后单个数据源的事务管理

主框架为 spring boot  + mybatis

AOP 切换

父 pom.xml 添加依赖包

<?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.top</groupId>
  <artifactId>demo-top</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>pom</packaging>
  <name>demo-top</name>
  
  <modules>
    <module>demo-dao</module>
    <module>demo-ser</module>
    <module>demo-web</module>
    <module>demo-pub</module>
  </modules>
  <!-- Maven 项目间的继承通过 parent 来表示 -->
	<parent>
		<!-- 被继承的父项目的全球唯一标识符 (目前理解为被继承目标所在位置的路径) -->
		<groupId>org.springframework.boot</groupId>
		<!-- 被继承的父项目的构件标识符 (目前理解为被继承目标对象的名称) -->
		<!-- spring-boot-starter-parent 该父类项目中定义了spring boot 版本的基础依赖以及一些默认配置类容 -->
		<artifactId>spring-boot-starter-parent</artifactId>
		<!-- (目标对象版本号) -->
		<version>1.5.14.RELEASE</version>
		<relativePath />
	</parent>
  
   <dependencies>
   	<dependency>
		<groupId>org.springframework.boot</groupId>
		<!-- spring-boot-starter-web 全栈web开发 嵌入了Tomcat,Spring MVC -->
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<!-- spring-boot-starter-tomcat是Spring Boot默认就会配置的,即上面说到的内嵌tomcat, 将其设置为provided是在打包时会将该包(依赖)排除,因为要放到独立的tomcat中运行,Spring 
			Boot内嵌的Tomcat是不需要用到的。 -->
		<artifactId>spring-boot-starter-tomcat</artifactId>
		<scope>provided</scope>
	</dependency>
	
	<dependency>
	    <groupId>org.springframework.boot</groupId>
	    <!-- spring boot AOP依赖 -->
	    <artifactId>spring-boot-starter-aop</artifactId>
	</dependency>
	
	<dependency>
		<groupId>org.mybatis.spring.boot</groupId>
		<!-- mybatis-spring-boot-starter spring boot 与 mybatis 整合 -->
		<artifactId>mybatis-spring-boot-starter</artifactId>
		<version>1.3.2</version>
	</dependency>

	<dependency>
		<groupId>mysql</groupId>
		<!-- mysql-connector-java 是MySQL的JDBC驱动包,用JDBC连接MySQL数据库时必须使用该jar包 -->
		<artifactId>mysql-connector-java</artifactId>
		<scope>runtime</scope>
	</dependency>
	
	<dependency>
		<groupId>com.alibaba</groupId>
		<!-- alibaba的druid数据库连接池 -->
		<artifactId>druid-spring-boot-starter</artifactId>
		<version>1.1.9</version>
	</dependency>
	
  	<dependency>
  		<groupId>org.springframework.boot</groupId>
  		<artifactId>spring-boot-configuration-processor</artifactId>
  		<optional>true</optional>
  	</dependency>
  	
  	<dependency>
	    <groupId>org.apache.shiro</groupId>
	    <!-- 权限验证框架 -->
	    <artifactId>shiro-core</artifactId>
	    <version>1.2.3</version>
	</dependency>
  </dependencies>
</project>

spring boot 启动类 与 application.properties

package com.demo.web;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * 首先要将spring boot自带的DataSourceAutoConfiguration禁掉,因为它会读取application.properties文件的spring.datasource.*属性并自动配置单数据源。
 */
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
/**
 * 设置事务执行顺序(需要在切换数据源之后)
 */
@EnableTransactionManagement(order = 2)	
@ComponentScan("com.demo")
@MapperScan(basePackages = "com.demo.dao")
/**
 *表示自己启动cglib代理,并且exposeProxy配置为true表示可以横切关注点中使用AopContext这个类
 */
@EnableAspectJAutoProxy(proxyTargetClass=true,exposeProxy=true)
public class SpringbootApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringbootApplication.class, args);
	}
	
}
server.port=8081

# survey库 数据源
datasource.one.survey.connect.driverClassName=com.mysql.jdbc.Driver
datasource.one.survey.connect.url=jdbc:mysql://localhost:3306/survey?useUnicode=true&autoReconnect=true&allowMultiQueries=true&useSSL=false&characterEncoding=utf-8
datasource.one.survey.connect.username=root
datasource.one.survey.connect.password=


# user库 数据源
datasource.two.user.connect.driverClassName=com.mysql.jdbc.Driver
datasource.two.user.connect.url=jdbc:mysql://localhost:3306/user?useUnicode=true&autoReconnect=true&allowMultiQueries=true&useSSL=false&characterEncoding=utf-8
datasource.two.user.connect.username=root
datasource.two.user.connect.password=

#最大连接池数量
datasource.druid.configure.maxActive=20
#最小连接池数量
datasource.druid.configure.minIdle=5
#初始化大小
datasource.druid.configure.initialSize=5
#最大连接时间
datasource.druid.configure.maxWait=60000
#配置一个连接在池中最小生存的时间,单位是毫秒
datasource.druid.configure.minEvictableIdleTimeMillis=300000
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
datasource.druid.configure.timeBetweenEvictionRunsMillis=60000
#建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
datasource.druid.configure.testWhileIdle=true
#验证连接有效与否的SQL,不同的数据配置不同
datasource.druid.configure.validationQuery=select 1
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙  
datasource.druid.configure.filters=stat,wall,log4j,logback
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录  
datasource.druid.configure.connectionProperties=druid.stat.mergeSql=true;

#druid 监控白名单ip
datasource.monitor.allow=127.0.0.1
#druid 监控 登录名 此处为加密字符串,在代码中获取该字符串进行节目并设置为 druid 监控登录名
datasource.monitor.loginUsername=w0rfNPkg+t8NIrnwiFoZDg==
#druid 监控 登录密码  此处为加密字符串,在代码中获取该字符串进行节目并设置为 druid 监控密码
datasource.monitor.loginPassword=dyzYlT6j0aV2cFqqIScNPA==
#druid 监控 是否可重制数据
datasource.monitor.resetEnable=false

mybatis.type-aliases-package=com.demo.pub.entity
mybatis.mapper-locations=classpath:mapper/**/*Mapper.xml
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.log-prefix=dao.

数据源主要实现类

项目启动时加载application.properties 数据源设置

package com.demo.pub.datasource;

import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;

/**
 * 数据源配置
 * 初始化时即加载
 */
@Configuration
public class DataSourceConfig {
	
	private Logger logger = LoggerFactory.getLogger(this.getClass());
	
	/**
	 * 读取prop Survey 的数据源连接配置 
	 */
	@Component
	@ConfigurationProperties(prefix="datasource.one.survey")
	protected class DataSourceSurvey{
		private Properties connect;

		public Properties getConnect() {
			return connect;
		}

		public void setConnect(Properties connect) {
			this.connect = connect;
		}
	}
	
	/**
	 * 读取prop user 的数据源连接配置 
	 */
	@Component
	@ConfigurationProperties(prefix="datasource.two.user")
	protected class DataSourceUser{
		private Properties connect;

		public Properties getConnect() {
			return connect;
		}

		public void setConnect(Properties connect) {
			this.connect = connect;
		}
	}
	
	/**
	 * 读取prop 对 Druid 的数据源设置
	 */
	@Component
	@ConfigurationProperties(prefix="datasource.druid")
	protected class DruidDataSourceConfigure{
		private Properties configure;

		public Properties getConfigure() {
			return configure;
		}

		public void setConfigure(Properties configure) {
			this.configure = configure;
		}
	}
	
	
	@Autowired
	private DataSourceSurvey survey;
	@Autowired
	private DataSourceUser user;
	@Autowired
	private DruidDataSourceConfigure dConfigure;
	
	/**
	 * 装载survey数据源的DataSource
	 */
	@Bean(name = "survey")	
	public  DataSource dataSource1() {		
		logger.info("加载survey配置......");
		try {
			if(dConfigure == null || dConfigure.getConfigure() == null){
				logger.info("加载druid 数据源配置异常......");
				throw new NullPointerException();
			}
			if(survey != null && survey.getConnect() != null){
				survey.getConnect().putAll(dConfigure.getConfigure());
				return DruidDataSourceFactory.createDataSource(survey.getConnect());
			}else{
				logger.info("加载survey 数据连接配置异常......");
				throw new NullPointerException();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		throw new NullPointerException();
	}
	

	/**
	 * 装载user数据源的DataSource
	 */
	@Bean(name = "user")	
	public DataSource dataSource2() {		
		logger.info("加载user配置......");
		try {
			if(dConfigure == null || dConfigure.getConfigure() == null){
				logger.info("加载druid 数据源配置异常......");
				throw new NullPointerException();
			}
			if(user != null && user.getConnect() != null){
				user.getConnect().putAll(dConfigure.getConfigure());
				return DruidDataSourceFactory.createDataSource(user.getConnect());
			}else{
				logger.info("加载user 数据连接配置异常......");
				throw new NullPointerException();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		throw new NullPointerException();
	}
	
	/**
	 * 注意@Primary标注多数据源,否则会产生实现类冲突。 优先使用,多数据源	
	 * @Primary:  意思是在众多相同的bean中,优先使用用@Primary注解的bean.
	 */
	@Bean(name="dynamicDataSource")	
	@Primary
	public DataSource dataSource() {
		
		DynamicDataSource dynamicDataSource = new DynamicDataSource();
		DataSource survey = dataSource1();
		DataSource user = dataSource2();
		//设置默认数据源
		dynamicDataSource.setDefaultTargetDataSource(survey);			
		//配置多数据源
		Map<Object,Object> map = new HashMap<Object, Object>();
		//key需要跟ThreadLocal中的值对应
		map.put("survey", survey);	
		map.put("user", user);		
		dynamicDataSource.setTargetDataSources(map);
		return dynamicDataSource;	
	}
	
	
	
	
	/***
	 * 允许访问监控的白名单
	 */
	@Value("${datasource.monitor.allow}")
	private String allow;
	/**
	 * 登录名
	 */
	@Value("${datasource.monitor.loginUsername}")
	private String loginUsername;
	/**
	 * 密码
	 */
	@Value("${datasource.monitor.loginPassword}")
	private String loginPassword;
	/**
	 * 是否能够重置数据
	 */
	@Value("${datasource.monitor.resetEnable}")
	private String resetEnable;
	
	/**
     * 配置监控服务器
     * @return 返回监控注册的servlet对象
     */
    @Bean
    public ServletRegistrationBean statViewServlet() {
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
        try {
        	// 添加IP白名单
            servletRegistrationBean.addInitParameter("allow",allow);
            // 添加控制台管理用户 这里采用了AES解密 是对配置文件Druid监控的登录名和密码解密测试是可使用明文
            servletRegistrationBean.addInitParameter("loginUsername", AESUtil.desEncrypt(loginUsername).trim());
            servletRegistrationBean.addInitParameter("loginPassword", AESUtil.desEncrypt(loginPassword).trim());
            // 是否能够重置数据
            servletRegistrationBean.addInitParameter("resetEnable", resetEnable);
		} catch (Exception e) {
			e.printStackTrace();
		}
        return servletRegistrationBean;
    }

    /**
     * 配置服务过滤器
     * @return 返回过滤器配置对象
     */
    @Bean
    public FilterRegistrationBean statFilter() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
        // 添加过滤规则
        filterRegistrationBean.addUrlPatterns("/*");
        // 忽略过滤格式
        filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*,");
        return filterRegistrationBean;
    }
	
}

设置数据源上下文 

package com.demo.pub.datasource;

/**
 * 设置数据源上下文 
 */
public class DataSourceContext {
	
	/**
	 * ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,
	 * 可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。
	 * 每个Thread线程内部都有一个Map。
		Map里面存储线程本地对象(key)和线程的变量副本(value)
		但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
		所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
	 * 
	 */
	private final static ThreadLocal<String> local = new ThreadLocal<String>();
	
	/**
	 * 
	 * set() 用于保存当前线程的副本变量值
	 */
	public static void setDataSource(String name) {
		local.set(name);
	}
	
	/**
	 * get()方法用于获取当前线程的副本变量值
	 */
	public static String getDataSource() {
		return local.get();
	}
	
	/**
	 * remove() 方法移除当前前程的副本变量值。
	 */
	public static void clearDataSource(){
		local.remove();
	}

}

 AbstractRoutingDataSource 实现类 ,实现AOP动态切换的关键

package com.demo.pub.datasource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * AbstractRoutingDataSource 实现类 ,实现AOP动态切换的关键
 *
 */
public class DynamicDataSource extends AbstractRoutingDataSource{
	
	private Logger logger = LoggerFactory.getLogger(this.getClass());
	

	@Override
	protected Object determineCurrentLookupKey() {
		String d = DataSourceContext.getDataSource();
		if(d == null){
			logger.info("未设置数据源,使用默认数据源......");
		}else{
			logger.info("数据源为{}",DataSourceContext.getDataSource());
		}
		return d;
	}

}

自定义AOP切入注解、此注解适用于 congtoller 层和 service层 的实现方法 

package com.demo.pub.datasource;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
 *@Retention: 定义注解的保留策略,注解会在class字节码文件中存在,在运行时可以通过反射获取到
 *@Target:定义注解的作用目标
 *@Documented  说明该注解将被包含在javadoc中
 *@Inherited 说明子类可以继承父类中的该注解 
 */
@Retention(RetentionPolicy.RUNTIME)  
@Target({
	ElementType.METHOD,
	ElementType.TYPE
}) 
@Documented 
@Inherited
public @interface DataSourceAop {
	String sourceName() ;
}

AOP切换数据源实现类

package com.demo.pub.datasource;


import java.lang.reflect.Method;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * AOP切换数据源
 */
@Aspect
/**
 * 设置AOP执行顺序(需要在事务之前)
 */
@Order(1)
@Component
public class DataSourceAspect {
	
	private Logger logger = LoggerFactory.getLogger(this.getClass());
	
	
	/**
	 * @Before 是在所拦截方法执行之前执行一段逻辑。
	 * @annotation  表示标注了某个注解的所有方法
	 */
	@Before("@annotation(DataSourceAop)")
	private void before2(JoinPoint point) {
		dynamicSwitch(point);
	}
	
	
	private void dynamicSwitch(JoinPoint point){
		//获得访问的方法名
		String method = point.getSignature().getName(); 
		//获得当前访问的类
		Class<?> classz = point.getTarget().getClass();   
		//得到方法的参数的类型
		MethodSignature methodSignature = (MethodSignature) point.getSignature();
		Class<?>[] parameterTypes = methodSignature.getMethod().getParameterTypes();
		
		try {        
			// 得到访问的方法对象
			Method m = classz.getMethod(method, parameterTypes);
			
			// 判断是否存在@DataSourceAop注解
            if (m.isAnnotationPresent(DataSourceAop.class)) {
            	DataSourceAop annotation = m.getAnnotation(DataSourceAop.class);
            	DataSourceContext.setDataSource(annotation.sourceName());
            	logger.info("===============上下文赋值完成:{}",annotation.sourceName());
            }
            
		} catch (Exception e) {
			logger.info("数据源切换异常");
			e.printStackTrace();          
		}    
	}
	
	/**
	 * 
	 * @author HJP
	 * @Description 清除本地线程副本数据源变量
	 * @After 在某个程序之后执行逻辑
	 * @return void
	 */
	@After("@annotation(DataSourceAop)")
	private void clearDataSource(){
		logger.info("清除本地线程副本数据源变量");
		DataSourceContext.clearDataSource();
	}

}

================================================================================================

                                                                                   以下为测试结果

================================================================================================

使用

在 控制层 或 服务层 的实现方法上加上 切入注解即可

不加注解则使用默认数据源

注意:

在同一类的某个方法,调用其他方法时数据源切换失败,这个问题也困扰了我一些时间,得到的问题原因是,在service里这样调方法是不会调用代理类中的方法,所以AOP就无法切入。总之必须要走代理才能切入。知道原因后在网上搜索到两种解决方式

1、使用spring 的 注入 @Autowired 注解 ,注入本类接口或实现类,在通过注入的对象去调用方法就能解决

package com.demo.ser.test.imp;

import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.demo.dao.test.TestDao;
import com.demo.pub.datasource.DataSourceAop;
import com.demo.ser.test.TestsImp;

@Service
public class Tests implements TestsImp{
	
	@Autowired
	private TestDao test;
	
	@Autowired
	private TestsImp testImp;
	
	@Override
	@DataSourceAop(sourceName = "survey")
	public String test() {
		return test.testA();
	}

	@Override
	@DataSourceAop(sourceName = "user")
	public String getTest3() {
		return test.testB();
	}

	@Override
	public String getTest4() {
		//解决在同一类调用其他方法数据源切换失败
		return testImp.test()+testImp.getTest3();
	}
	
	@Override
	@Transactional
	@DataSourceAop(sourceName = "user")
	public int update1() {
		test.testD();
		test.testE();
//		throw new NullPointerException();
		return 1;
	}
}

执行结果

2.启动cglib代理,使用 EnableAspectJAutoProxy注解 表示自己启动cglib代理,并且exposeProxy配置为true表示可以横切关注点中使用AopContext这个类。

controller

package com.demo.web.test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.demo.pub.datasource.DataSourceAop;
import com.demo.ser.test.TestsImp;

@RestController
@RequestMapping("/tests")
public class TestS {
	
	@Autowired
	private TestsImp test;
	
	
	
	@RequestMapping(value="/test1",method=RequestMethod.GET)
	public String test1(){
		try {
			
			return test.getTest4();
		} catch (Exception e) {
			e.printStackTrace();
			return"5555";
		}
	}

}

serice 层 测试代码

package com.demo.ser.test.imp;

import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.demo.dao.test.TestDao;
import com.demo.pub.datasource.DataSourceAop;
import com.demo.ser.test.TestsImp;

@Service
public class Tests implements TestsImp{
	
	@Autowired
	private TestDao test;
	
	@Override
	@DataSourceAop(sourceName = "survey")
	public String test() {
		return test.testA();
	}

	@Override
	@DataSourceAop(sourceName = "user")
	public String getTest3() {
		return test.testB();
	}

	@Override
	public String getTest4() {
		//解决在同一类调用其他方法数据源切换失败
		return((TestsImp) AopContext.currentProxy()).test()+((TestsImp) AopContext.currentProxy()).getTest3();  
	}
	
	@Override
	@Transactional
	@DataSourceAop(sourceName = "user")
	public int update1() {
		test.testD();
		test.testE();
//		throw new NullPointerException();
		return 1;
	}
}

 

启动时日志输出

执行结果

 

================================================================================================

 开启 Druid 监控 打开浏览器 输入地址 如: http://localhost:8081/druid/datasource.html

输入配置的Druid 监控登录名和密码即可

注意:在 数据源 选项中会出现以下提示

随便执行一个方法sql 就OK了

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值