spring boot + mybatis +aop 注解实现多数据源动态切换

多数据源配置,在实际开发中,我们一个项目可能会用到多个数据库,通常一个数据库对应一个数据,而且在大数据量的业务下通常都会有多个数据源的。最近在有一个Spring cloud+ sping. Boot + mybatis 框架的多数据源配置切换的业务,所以就研究了一下关于Springboot+ mybatis 多数据源切换,具体没有经过实际业务的测试,只是记录一下实现及学习的心得。请见谅。

不多说了,以下是具体的实现过程:

一 .构建一个 “timer-service “ maven 项目

此功能的实现代码只有common 包下的代码,datasource 包下为数据源加载信息配置,annotanion 和advice 是实现了基于aop的通过注解的方式动态切换数据源。

1.配置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.hdsxtech</groupId>
	<artifactId>timer-service</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>timer-service</name>
	<description>timer-service</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.18.BUILD-SNAPSHOT</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
		<spring-cloud.version>Edgware.BUILD-SNAPSHOT</spring-cloud.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>com.oracle</groupId>
			<artifactId>ojdbc6</artifactId>
			<version>11.2.0.3</version>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.1.10</version>
		</dependency>
		<!-- 配置log4j 日志,过滤掉spring-boot 默认日志系统 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
			<exclusions>
				<exclusion>
					<groupId>org.springframework.boot</groupId>
					<artifactId>spring-boot-starter-logging</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-log4j2</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
       <!-- 健康检查 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid-spring-boot-starter</artifactId>
			<version>1.1.10</version>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.3.1</version>
		</dependency>
		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis</artifactId>
			<version>3.4.4</version>
		</dependency>
	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

	<repositories>
		<repository>
			<id>spring-snapshots</id>
			<name>Spring Snapshots</name>
			<url>https://repo.spring.io/snapshot</url>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</repository>
		<repository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</repository>
	</repositories>

	<pluginRepositories>
		<pluginRepository>
			<id>spring-snapshots</id>
			<name>Spring Snapshots</name>
			<url>https://repo.spring.io/snapshot</url>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</pluginRepository>
		<pluginRepository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</pluginRepository>
	</pluginRepositories>


</project>

二 .配置文件

pom.xml 添加了项目所需用的jar 包以来,接下来是配置文件信息,在 application.properties 中做以下配置

eureka.client.serviceUrl.defaultZone=http://localhost:8001/eureka/,http://localhost:8001/eureka/
eureka.instance.preferIpAddress=false
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.healthcheck.enabled=true
eureka.instance.lease-expiration-duration-in-seconds=3
eureka.instance.lease-renewal-interval-in-seconds=1

spring.application.name=timer-service
server.port=9099



jdbc.type=com.alibaba.druid.pool.DruidDataSource
master.jdbc.driver-class-name=oracle.jdbc.driver.OracleDriver
master.jdbc.url=jdbc:oracle:thin:@localhost:1521:orcl
master.jdbc.username=ss_t1
master.jdbc.password=ss_t1


slave.jdbc.driver-class-name=oracle.jdbc.driver.OracleDriver
slave.jdbc.url=jdbc:oracle:thin:@localhost:1521:orcl
slave.jdbc.username=ss_t2
slave.jdbc.password=ss_t2

# mybatis 配置信息
mybatis.mapper-locations=classpath*:mybatis/*.xml
# sql 日志
logging.level.com.hdsxtech.smsoot_timer.mapper=debug

从第一行开始 到 jdbc.type 这行上部分主要为 sping cloud 所需要的配置信息,可以不用做过多的了解。之后为 配置了两个数据源,master 库和 slave 库,master为主库,也是默认库。

三.上代码,加载并配置数据源

1.DatabaseType

 /**
 * Copyright (c)  FESCOTech Team
 * All rights reserved.
 *
 * This file DatabaseType.java creation date: [2018年11月15日 上午8:23:08] by 姓名
 * http://www.fescoetc.com.cn
 */

package com.hdsxtech.smsoot_timer.common.datasource;

/** 
 * <descption>列出所有的数据源key</descption> 
 * 这里的数据源与数据库是一对一的
 * @author mawen
 * @version 1.0.0
 */

public enum DatabaseType {
	MASTER,SLAVE
}

作用:列举数据源的key.

2.DatabaseContextHolder

/**
 * Copyright (c)  FESCOTech Team
 * All rights reserved.
 *
 * This file DatabaseContextHolder.java creation date: [2018年11月15日 上午8:25:15] by 姓名
 * http://www.fescoetc.com.cn
 */

package com.hdsxtech.smsoot_timer.common.datasource;

/** 
 * <descption>保存一个线程安全的DatabaseContextHolder</descption> 
 * @author maen
 * @version 1.0.0
 */

public class DatabaseContextHolder {

	// 线程局部变量
	private static final ThreadLocal<DatabaseType> contextHolder = new ThreadLocal<DatabaseType>();

	// 往线程里边set 数据类型
	public static void setDatabaseType(DatabaseType type){
		if(type ==null) throw new NullPointerException();
		contextHolder.set(type);
	}
	
	// 从容器中获取数据类型
	public static DatabaseType getDatabaseType(){
		return contextHolder.get() ==null ?DatabaseType.MASTER:contextHolder.get();
	}
	
	// 清空容器中的数据类型
	public static void clear(){
		contextHolder.remove();
	}
}

作用:构建一个DatabaseType 容器,并提供了向其中设置 ,获取,清除 DatabaseType 的方法。

3.DynamicDataSource

/**
 * Copyright (c)  FESCOTech Team
 * All rights reserved.
 *
 * This file DynamicDataSourec.java creation date: [2018年11月15日 上午8:34:37] by 姓名
 * http://www.fescoetc.com.cn
 */

package com.hdsxtech.smsoot_timer.common.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/** 
 * <descption>动态数据源(需要继承AbstractRoutingDataSource)</descption> 
 * @author mawen
 * @version 1.0.0
 */

public class DynamicDataSource extends AbstractRoutingDataSource{

 	@Override
	protected Object determineCurrentLookupKey() {
		return DatabaseContextHolder.getDatabaseType();
	}

}

作用:使用DatabaseContextHolder 获取当前线程的DatabaseType

4.DataSourceConfiguration

/**
 * Copyright (c)  FESCOTech Team
 * All rights reserved.
 *
 * This file DataSourceConfiguration.java creation date: [2018年11月15日 上午8:37:19] by 姓名
 * http://www.fescoetc.com.cn
 */

package com.hdsxtech.smsoot_timer.common.datasource;

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

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

/** 
 * <descption>配置并创建数据源</descption> 
 * @author mawen
 * @version 1.0.0
 */
@Configuration
public class DataSourceConfiguration {

	private static final Logger LOGGER = LoggerFactory.getLogger(DataSourceConfiguration.class) ;
	
	@Autowired
	private Environment env;
	
	@Value("${jdbc.type}")
	private Class<? extends DataSource> dataSourceType;
	
	@Bean(name="masterDataSource")
	@ConfigurationProperties(prefix="master.jdbc")
	@Primary
	public  DataSource masterDataSource(){
		DataSource masterDataSource = DataSourceBuilder.create().type(dataSourceType).build();
		LOGGER.info("========= MASTER :{} =======",masterDataSource);
		return masterDataSource;
	}
	
	@Bean(name="slaveDataSource")
	@ConfigurationProperties(prefix="slave.jdbc")
	public DataSource slaveDataSource(){
		DataSource slaveDataSource = DataSourceBuilder.create().type(dataSourceType).build();
		LOGGER.info("========= SLAVE :{} ========",slaveDataSource);
		return slaveDataSource;
	}
	
	/**
	 * @Primary 给注解表示在同一个接口有多个实现类可以注入的时候,默认选择那个,而不是让@Autowied 注解报错
	 * @Qualifier 根据名称进行注入,通常是在具有相同的多个类型的实力的一个注入(列如有多个DataSource 的类型)
	 */
	@Bean
	public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
			@Qualifier("slaveDataSource") DataSource slaveDataSource){
		Map<Object,Object> targetDataSources = new HashMap<>();
		targetDataSources.put(DatabaseType.MASTER, masterDataSource);
		targetDataSources.put(DatabaseType.SLAVE, slaveDataSource);
		
		DynamicDataSource dataSource = new DynamicDataSource();
		dataSource.setTargetDataSources(targetDataSources); // 给方法是AbstractRoutingDataSource 的方法
		dataSource.setDefaultTargetDataSource(masterDataSource); // 默认的datasource 设置为  masterDataSource
		return dataSource;
	}
	
	/**
	 * 
	 * <descption> 根据数据源创建SqlSessionFactory </descption>
	 * 
	 * @param 
	 * @return
	 * @throws Exception 
	 */
	@Bean
	public SqlSessionFactory seqSessionFactory(DynamicDataSource ds) throws Exception{
		SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
		fb.setDataSource(ds);  // 指定数据源(这个必须有,否则报错)
		
		// 下边这两句仅仅用于*.xml文件,如果这个持久层操作不需要使用的xml 的话(只用注解就可以搞定) ,则不加
		fb.setTypeAliasesPackage("mybatis.typeAliasesPackage");  // 指定基包
		fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapper-locations")));
		return fb.getObject();
	}
	
	/**
	 * 
	 * <descption> 配置事务管理 </descption>
	 * 
	 * @param 
	 * @return
	 */
	@Bean
	public DataSourceTransactionManager tracsactionManager(DynamicDataSource ds){
		return new DataSourceTransactionManager(ds);
	}
}

作用:

  • 3.1 通过读取application.properties 文件中的两个数据源(master,slave )
  • 3.2 使用以上生成的两个数据源构造动态数据源dataSource
  1. @Primary:指定在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是 @Autowire注解报错(一般用于多数据源的情况下)
  2. @Qualifier:指定名称的注入,当一个接口有多个实现类的时候使用(在本例中,有两个DataSource类型的实例,需要指定名称注入)
  3. @Bean:生成的bean实例的名称是方法名(例如上边的@Qualifier注解中使用的名称是前边两个数据源的方法名,而这两个数据源也是使用@Bean注解进行注入的)
  • 3.3 通过动态数据源构造SqlSessionFactory和事务管理器(如果不需要事务,后者可以去掉)

四.注解

1.Write 写注解

/**
 * Copyright (c)  FESCOTech Team
 * All rights reserved.
 *
 * This file Write.java creation date: [2018年11月15日 上午9:42:32] by 姓名
 * http://www.fescoetc.com.cn
 */

package com.hdsxtech.smsoot_timer.common.annotation;

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

/** 
 * <descption>写</descption> 
 * @author mawen
 * @version 1.0.0
 */
@Target({ElementType.METHOD,ElementType.TYPE}) // 该注解用在方法上
@Retention(RetentionPolicy.RUNTIME) // 在运行时加载
public @interface Write {

}

作用:此注解标识 主数据库

2.Read 读注解

/**
 * Copyright (c)  FESCOTech Team
 * All rights reserved.
 *
 * This file ReadOnlyConn.java creation date: [2018年11月15日 上午9:39:51] by 姓名
 * http://www.fescoetc.com.cn
 */

package com.hdsxtech.smsoot_timer.common.annotation;

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

/** 
 * <descption>只读</descption> 
 * @author mawen
 * @version 1.0.0
 */
@Target({ElementType.METHOD,ElementType.TYPE}) // 该注解用在方法上
@Retention(RetentionPolicy.RUNTIME) // 在运行时加载
public @interface Read {

}

作用:此注解表示为从库

五.加载注解

1. WriteAdvice // 加载Write 注解

/**
 * Copyright (c)  FESCOTech Team
 * All rights reserved.
 *
 * This file WriteAdvice.java creation date: [2018年11月15日 上午9:58:27] by 姓名
 * http://www.fescoetc.com.cn
 */

package com.hdsxtech.smsoot_timer.common.advice;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

import com.hdsxtech.smsoot_timer.common.annotation.Write;
import com.hdsxtech.smsoot_timer.common.datasource.DatabaseContextHolder;
import com.hdsxtech.smsoot_timer.common.datasource.DatabaseType;
 

/** 
 * <descption>执行写注解</descption> 
 * @author mawen
 * @version 1.0.0
 */
@Aspect
@Component
@EnableAspectJAutoProxy(exposeProxy=true,proxyTargetClass=true)
public class WriteAdvice implements Ordered {

	public static final Logger LOGGER = LoggerFactory.getLogger(WriteAdvice.class);
	
	@Around("@annotation(write)") // 在注解上加入切人点语法,实现方法
	public Object proceed(ProceedingJoinPoint proceedingJoinPoint,Write write) throws Throwable{
		
		try {
			LOGGER.info("-----------------set database connection write only---------------------");
			DatabaseContextHolder.setDatabaseType(DatabaseType.MASTER);
			return proceedingJoinPoint.proceed();  // 让这个方法执行完毕
		}finally{
			DatabaseContextHolder.clear();
			LOGGER.info("---------------clear database connection---------------");
		}
	}
	
	
 	@Override
	public int getOrder() {
		return 1;
	}

}

2.ReadAdvice // 加载read 注解

/**
 * Copyright (c)  FESCOTech Team
 * All rights reserved.
 *
 * This file ReadAdvice.java creation date: [2018年11月15日 上午9:43:46] by 姓名
 * http://www.fescoetc.com.cn
 */

package com.hdsxtech.smsoot_timer.common.advice;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

import com.hdsxtech.smsoot_timer.common.annotation.Read;
import com.hdsxtech.smsoot_timer.common.datasource.DatabaseContextHolder;
import com.hdsxtech.smsoot_timer.common.datasource.DatabaseType;

 

/** 
 * <descption>执行只读注解</descption> 
 * @author maen
 * @version 1.0.0
 */
@Aspect
@Component
@EnableAspectJAutoProxy(exposeProxy=true,proxyTargetClass=true)
public class ReadAdvice implements Ordered{

	public static final Logger LOGGER = LoggerFactory.getLogger(ReadAdvice.class);
	
	@Around("@annotation(read)") // 在注解上加入切人点语法,实现方法
	public Object proceed(ProceedingJoinPoint proceedingJoinPoint,Read read) throws Throwable{
		try {
			LOGGER.info("----------------set database connection read only----------------------");
			DatabaseContextHolder.setDatabaseType(DatabaseType.SLAVE);
			return proceedingJoinPoint.proceed(); // 让这个方法执行完
		}finally{
			DatabaseContextHolder.clear();
			LOGGER.info("-----------------clear database connection --------------------");
		}
	}
	
	
	
	@Override
	public int getOrder() {
		return 1;
	}

}

以上的代码是用来实现 多数据源配置加载,以及通过注解来实现数据源的动态切换功能。

六.使用

1.启动类 TimerApplication

package com.hdsxtech.smsoot_timer;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.hdsxtech.*.mapper")
public class TimerApplication {

	public static void main(String[] args) {
		SpringApplication.run(TimerApplication.class, args);
	}
}

2. TestBMapper

package com.hdsxtech.smsoot_timer.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

import com.hdsxtech.smsoot_timer.entity.TestB;

@Mapper
public interface TestBMapper {

	List<TestB> queryAll();

	void save(TestB testB);

	void update(TestB testB);

}

3.TestBServiceImpl

package com.hdsxtech.smsoot_timer.service.impl;

import java.util.List;

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

import com.hdsxtech.smsoot_timer.common.annotation.Read;
import com.hdsxtech.smsoot_timer.common.annotation.Write;
import com.hdsxtech.smsoot_timer.entity.TestB;
import com.hdsxtech.smsoot_timer.mapper.TestBMapper;
import com.hdsxtech.smsoot_timer.service.TestBService;

@Service
@Transactional
public class TestBServiceImpl implements TestBService {

	@Autowired
	TestBMapper testBMapper;
	
	@Read
	@Override
	public List<TestB> queryAll() {
		return testBMapper.queryAll();
	}

	@Read
	@Override
	public void save(List<TestB> testbList) {
		for (TestB testB : testbList) {
			testBMapper.save(testB);
		}
		
	}
	
	@Read
	@Override
	public void update(List<TestB> testbList) {
		 
		for (TestB testB : testbList) {
			testBMapper.update(testB);
		}
	}

}

注意:在调用mapper 接口的方法上,需要通过注解来注入对应的数据源,否则就会已默认的 master 数据源操作数据。

4.TestBService

package com.hdsxtech.smsoot_timer.service;

import java.util.List;

import com.hdsxtech.smsoot_timer.entity.TestB;

public interface TestBService {

	List<TestB> queryAll();
	
	void save(List<TestB> testbList);

	void update(List<TestB> testbList);
}

文档写的不是太好,请大家多多包含。

源代码

码云:https://gitee.com/sugnup/timer-service.git

个人博客:http://121.42.10.167

转载于:https://my.oschina.net/hen/blog/2985334

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值