该代码已运用正式项目中此处为测试demo,我自己理解得也不深但功能实现了,不足点还望大神指出
不足点,未能实现两个数据源之间的事务,只能实现切换后单个数据源的事务管理
主框架为 spring boot + mybatis
AOP 切换
父 pom.xml 添加依赖包
4.0.0
com.top
demo-top
0.0.1-SNAPSHOT
pom
demo-top
demo-dao
demo-ser
demo-web
demo-pub
org.springframework.boot
spring-boot-starter-parent
1.5.14.RELEASE
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-tomcat
provided
org.springframework.boot
spring-boot-starter-aop
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.2
mysql
mysql-connector-java
runtime
com.alibaba
druid-spring-boot-starter
1.1.9
org.springframework.boot
spring-boot-configuration-processor
true
org.apache.shiro
shiro-core
1.2.3
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
设置数据源上下文
package com.demo.pub.datasource;
/**
* 设置数据源上下文
*/
public class DataSourceContext {
/**
* ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,
* 可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。
* 每个Thread线程内部都有一个Map。
Map里面存储线程本地对象(key)和线程的变量副本(value)
但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
*
*/
private final static ThreadLocal local = new ThreadLocal();
/**
*
* 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了