Spring AOP实现数据源切换(Spring Boot项目)
感谢网上诸多前辈的文章让我能写出通过Aop实现动态数据源切换,总结一下自己的学习,也是踩坑过来的,上代码。
PS:请注意看代码中的注释,注释方便你的理解。
1、创建application.yml文件,配置如下:(注意文件名千万别写错了)
#logging日志配置 logging: level: root: WARN org: springframework: web: DEBUG ##指向mapper的xml文件位置 mybatis: # 配置mapper的扫描,找到所有的mapper.xml映射文件 mapperLocations: classpath:mybatis/mapper/*.xml # 加载全局的配置文件 configLocation: mybatis/mybatis-config.xml spring: datasource: ## master 数据源配置 master: url: jdbc:mysql://***.**.**.**:3306/NIHAO?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true username: root password: 123456 driverClassName: com.mysql.jdbc.Driver ## cluster 数据源配置 slave: url: jdbc:mysql://***.**.**.**:3306/NIHAO?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true username: root password: 123456 driverClassName: com.mysql.jdbc.Driver
2、配置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.example</groupId> <artifactId>aop</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>aop</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</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> </properties> <dependencies> <!-- 加载properties文件依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web-services</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- 阿里线程池以及json转换工具--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.11</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.31</version> </dependency> <!-- 字符串工具 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.7</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-test</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>RELEASE</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
3、在resources文件夹下创建mapper文件,并创建mapper映射文件,例如创建UserMapping.xml:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.aop.dao.UserDao"> <!-- 创建用户(Create) --> <insert id="insertUser" parameterType="com.example.aop.entity.UserBean"> insert into users(name,password) values(#{name},#{password}) </insert> <!-- 删除用户(Delete) --> <update id="deleteUserById"> delete from users where id=#{id} </update> <!-- 修改用户基本资料(Update) --> <update id="updateUserById" parameterType="com.example.aop.entity.UserBean"> update users set name=#{name} where id=#{id} </update> <!-- 查询全部用户 --> <select id="getAllUser" resultType="com.example.aop.entity.UserBean"> select * from users </select> </mapper>
开始正式编码:
数据源代码:
4、创建动态数据源DynamicDataSource.java
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import javax.annotation.Resource; import javax.sql.DataSource; public class DynamicDataSource extends AbstractRoutingDataSource { private static final Logger log = LoggerFactory.getLogger(DynamicDataSource.class); @Override protected Object determineCurrentLookupKey() { log.debug("数据源为:====", DynamicDataSourceHolder.getDataSourceKey()); System.out.println("数据源为:===="+DynamicDataSourceHolder.getDataSourceKey()); return DynamicDataSourceHolder.getDataSourceKey(); } }
5、创建动态数据源处理器DynamicDataSourceHolder.java
import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DynamicDataSourceHolder { public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceHolder.class); //使用线程安全的ThreadLocal记录当前线程数据源key private static final ThreadLocal<String> holder = new ThreadLocal<String>(); /** * 设置数据库key * @param key */ public static void putDataSourceKey(String key){ log.debug("切换到{}数据源", key); System.out.println("数据源:" + key); holder.set(key); } /** * 获取数据源key * @return */ public static String getDataSourceKey(){ return holder.get(); } /** * 删除数据源key */ public static void removeDataSourceKey(){ holder.remove(); } }
6、重写数据源默认配置,创建DataSourceConfig.java类
import com.alibaba.druid.pool.DruidDataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * 数据源配置 */ @Configuration @EnableTransactionManagement @MapperScan(basePackages = {"com.example.aop.dao"}, sqlSessionFactoryRef = "sqlSessionFactory") public class DataSourceConfig { // 配置mapper的扫描,找到所有的mapper.xml映射文件 @Value("${mybatis.mapperLocations}") private String mapperLocations; // 加载全局的配置文件 @Value("${mybatis.configLocation}") private String configLocation; /** * 生成主数据源bean,通过@ConfigurationProperties导入数据源配置 * @return */ @Bean(name = "masterDataSource") @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource masterDataSource(){ DruidDataSource masterDataSource = DataSourceBuilder.create().type(com.alibaba.druid.pool.DruidDataSource.class).build(); masterDataSource.setName("masterDataSource"); return masterDataSource; } /** * 生成从数据源bean,通过@ConfigurationProperties导入数据源配置 * @return */ @Bean(name = "slaveDataSource") @ConfigurationProperties(prefix = "spring.datasource.slave") public DataSource slaveDataSource(){ DruidDataSource slaveDataSource = DataSourceBuilder.create().type(com.alibaba.druid.pool.DruidDataSource.class).build(); slaveDataSource.setName("slaveDataSource"); return slaveDataSource; } /** * 动态数据源: 通过AOP在不同数据源之间动态切换 * @return */ @Bean(name = "dataSource") public DataSource dataSource() { DynamicDataSource dynamicDataSource = new DynamicDataSource(); // 默认数据源 dynamicDataSource.setDefaultTargetDataSource(masterDataSource()); // 配置多数据源 Map<Object, Object> dsMap = new HashMap(2); dsMap.put("masterDataSource", masterDataSource()); dsMap.put("slaveDataSource", slaveDataSource()); dynamicDataSource.setTargetDataSources(dsMap); return dynamicDataSource; } @Bean public PlatformTransactionManager txManager(@Qualifier("dataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } /** * @param dynamicDataSource * @return * @throws Exception */ @Bean(name="sqlSessionFactory") public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dynamicDataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dynamicDataSource); //扫描mapper配置 sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations)); //扫描mybatis配置文件 sqlSessionFactoryBean.setConfigLocation(new ClassPathResource(configLocation) ); return sqlSessionFactoryBean.getObject(); } /** * @param sqlSessionFactory * @return * @throws Exception */ @Bean public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { SqlSessionTemplate template = new SqlSessionTemplate(sqlSessionFactory); // 使用上面配置的Factory return template; } }
7、关键点创建DataSourceAspect.java
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.annotation.Pointcut; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.apache.commons.lang3.StringUtils; @Aspect @Order(1) @Component public class DataSourceAspect { //这两个必须与DatasourceConfig类中dataSource() 方法中hashmap的key一致,他通过key来判断数据源 private static final String MASTER = "masterDataSource"; private static final String SLAVE = "slaveDataSource"; private static final String[] defaultSlaveMethod = new String[]{ "query", "find", "get" }; //切换放在mapper接口的方法上,所以这里要配置AOP切面的切入点 @Pointcut("execution( * com.example.aop.service.*.*(..))") public void dataSourcePointCut() { } @Before("dataSourcePointCut()") public void before(JoinPoint joinPoint) { // 获取到当前执行的方法名 String methodName = joinPoint.getSignature().getName(); boolean isSlave = false; isSlave = isSlave(methodName); System.out.println("是否从库:"+isSlave); if (isSlave) { // 标记为读库 DynamicDataSourceHolder.putDataSourceKey(SLAVE); } else { // 标记为写库 DynamicDataSourceHolder.putDataSourceKey(MASTER); } } //执行完切面后,将线程共享中的数据源名称清空 @After("dataSourcePointCut()") public void after(JoinPoint joinPoint){ System.out.println("执行完毕!"); DynamicDataSourceHolder.removeDataSourceKey(); } /** * 判断是否为读库 * * @param methodName * @return */ private Boolean isSlave(String methodName) { // 方法名以query、find、get开头的方法名走从库 return StringUtils.startsWithAny(methodName, defaultSlaveMethod); } }
8、配置springboot启动类:
使用exclude即启动的时候不再加载springboot默认的数据源配置类
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) public class AopApplication { public static void main(String[] args) { SpringApplication.run(AopApplication.class, args); } }
通过以上的代码编写已经完成了动态数据源的切换,现在来验证:
实体类:
9、创建entity实体,UserBean.java
public class UserBean { private int id; private String name; private String password; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { final StringBuilder sb = new StringBuilder("{"); sb.append("\"id\":") .append(id); sb.append(",\"name\":\"") .append(name).append('\"'); sb.append(",\"password\":\"") .append(password).append('\"'); sb.append('}'); return sb.toString(); } }
Dao层
10、创建dao,UserDao.java
import com.example.aop.entity.UserBean; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; import java.util.List; @Repository public interface UserDao { /** * @param userBean * @return */ int insertUser(UserBean userBean); /** * @param id * @return */ int deleteUserById(@Param("id") int id); /** * @param userBean * @return */ int updateUserById(UserBean userBean); /** * @return */ List<UserBean> getAllUser(); }
Service层:
11、创建service接口,UserService.java
package com.example.aop.service; import com.example.aop.entity.UserBean; import java.util.List; public interface UserService { /** * @param userBean * @return */ int insertUser(UserBean userBean); /** * @param id * @return */ int deleteUserById(int id); /** * @param userBean * @return */ int updateUserById(UserBean userBean); /** * @return */ List<UserBean> getAllUser(); }
11、创建UserService实现类,UserServiceImpl.java
import com.example.aop.dao.UserDao; import com.example.aop.entity.UserBean; import com.example.aop.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.validation.constraints.Max; import java.util.List; @Service @Transactional public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Override public int insertUser(UserBean userBean) { return userDao.insertUser(userBean); } @Override public int deleteUserById(int id) { return userDao.deleteUserById(id); } @Override public int updateUserById(UserBean userBean) { return userDao.updateUserById(userBean); } @Override public List<UserBean> getAllUser() { return userDao.getAllUser(); } }
测试类:
12、创建测试类
@RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = AopApplication.class) public class UserServiceTest { @Autowired private UserService userService; @Test public void testDynamicDatasource() { UserBean userBean = new UserBean(); userBean.setName("tudou"); userBean.setPassword("111111"); userService.insertUser(userBean); System.out.println(userService.getAllUser()); System.out.println(userService.insertUser(userBean)); System.out.println(userService.getAllUser()); } }