1. 程式人生 > >Spring AOP實現多資料來源切換

Spring AOP實現多資料來源切換

有時候我們會遇到這樣的場景:一個應用系統中存在多個數據源,需要根據不同業務場景進行臨時切換。比如讀寫分離(也可以考慮使用Mycat等資料庫中介軟體)等。

Spring提供了動態資料來源的功能,可以讓我們實現在對資料庫操作前進行切換。

下面我們演示怎麼在專案中配置多資料來源並根據不用業務場景進行切換(本文涉及到Spring Boot和Spring Data Jpa,相關內容及配置不做詳解)。

1、在MySQL上面建立兩個資料庫(實際中可能在不同伺服器),並建立相同的一張表student。

 附上建表SQL:

DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `name` varchar(255) DEFAULT NULL COMMENT '姓名',
  `sex` tinyint(1) DEFAULT NULL COMMENT '性別:1男2女',
  `age` tinyint(3) DEFAULT NULL COMMENT '年齡',
  `birthday` datetime DEFAULT NULL COMMENT '生日',
  `address` varchar(255) DEFAULT NULL COMMENT '住址',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=latin1;

2、建立對應的學生實體類。

/**
 * 學生實體類
 * @author z_hh  
 * @date 2018年11月30日
 */
@Getter
@Setter
@Builder
@ToString
@Entity
public class Student implements Serializable {

	private static final long serialVersionUID = -5636317592972887581L;

	/** 主鍵 */
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	/** 姓名 */
	private String name;
	
	/** 性別 */
	private Integer sex;
	
	/** 年齡 */
	private Integer age;
	
	/** 生日 */
	private Date birthday;
	
	/** 地址 */
	private String address;
	
	public Student() {
	}

	public Student(Long id, String name, Integer sex, Integer age, Date birthday, String address) {
		super();
		this.id = id;
		this.name = name;
		this.sex = sex;
		this.age = age;
		this.birthday = birthday;
		this.address = address;
	}
	
}

3、配置兩個不同資料來源的連線資訊。

4、自定義動態資料來源(關鍵)。 

這裡定義一個動態資料來源類,繼承自AbstractRoutingDataSource並重寫determineCurrentLookupKey方法,將資料來源的切換粒度設定為執行緒。定義兩個泛型(因為系統使用兩個資料來源)以表示資料來源的型別,並提供執行緒可見變數CONTEX_THOLDER的獲取和修改方法。

/**
 * 動態資料來源
 * @author z_hh  
 * @date 2018年11月30日
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
	
	/**
	 * 本地執行緒可見變數,儲存當前執行緒使用了哪一種資料來源,初始設為Master
	 */
	private static final ThreadLocal<DatabaseType> CONTEX_THOLDER = new ThreadLocal<DatabaseType>() {
		protected DatabaseType initialValue() {
			return DatabaseType.MASTER;
		};
	};

	/**
	 * 重寫AbstractRoutingDataSource方法,Spring從這裡獲取當前執行緒的資料來源型別
	 */
	@Override
	protected Object determineCurrentLookupKey() {
		return CONTEX_THOLDER.get();
	}

	/**
	 * 資料來源型別列舉類:Master主庫,Slave從庫
	 * @author z_hh  
	 * @date 2018年11月30日
	 */
	public static enum DatabaseType {
		MASTER, SLAVE
	}

	/**
	 * 將當前執行緒資料來源型別設為Master
	 */
	public static void master() {
		CONTEX_THOLDER.set(DatabaseType.MASTER);
	}

	/**
	 * 將當前執行緒資料來源型別設為Slave
	 */
	public static void slave() {
		CONTEX_THOLDER.set(DatabaseType.SLAVE);
	}

	/**
	 * 設定當前執行緒資料來源型別
	 */
	public static void setDatabaseType(DatabaseType type) {
		CONTEX_THOLDER.set(type);
	}

	/**
	 * 獲取當前執行緒資料來源型別
	 */
	public static DatabaseType getType() {
		return CONTEX_THOLDER.get();
	}
}

5、資料來源配置(關鍵)。

這裡建立動態資料來源物件,並建立兩個型別的資料來源作為其目標資料來源,將其預設資料來源設定為Master。最後交由Spring IOC容器管理。

/**
 * DataResource配置類
 * @author z_hh  
 * @date 2018年11月30日
 */
@Configuration
public class DataSourceConf {
	
	/**
	 * 將動態資料來源注入Spring IOC容器
	 * @return
	 */
	@Bean
	public DataSource dynamicDataSource() {
		DataSource master = master();
		DataSource slave = slave();
		Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
		targetDataSources.put(DynamicDataSource.DatabaseType.MASTER, master);
		targetDataSources.put(DynamicDataSource.DatabaseType.SLAVE, slave);
		DynamicDataSource dataSource = new DynamicDataSource();
		dataSource.setTargetDataSources(targetDataSources);// 該方法是AbstractRoutingDataSource的方法
		dataSource.setDefaultTargetDataSource(master);
		return dataSource;
	}

	/**
	 * 建立Master資料來源物件
	 * @return
	 */
	public DataSource master() {
		HikariDataSource ds = new HikariDataSource();
        ds.setJdbcUrl(env.getProperty("master.datasource.url"));
        ds.setUsername(env.getProperty("master.datasource.username"));
        ds.setPassword(env.getProperty("master.datasource.password"));
        ds.setDriverClassName(env.getProperty("master.datasource.driver-class-name"));
		return ds;
	}

	/**
	 * 建立Slave資料來源物件
	 * @return
	 */
	public DataSource slave() {
		HikariDataSource ds = new HikariDataSource();
        ds.setJdbcUrl(env.getProperty("slave.datasource.url"));
        ds.setUsername(env.getProperty("slave.datasource.username"));
        ds.setPassword(env.getProperty("slave.datasource.password"));
        ds.setDriverClassName(env.getProperty("slave.datasource.driver-class-name"));
		return ds;
	}

	@Autowired
	private Environment env;

}

6、定義兩個註解,分別用於使用不同型別資料來源的方法上面。

/**
 * 使用主庫的註解
 * @author z_hh  
 * @date 2018年11月30日
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Master {
}
/**
 * 使用從庫的註解
 * @author z_hh  
 * @date 2018年11月30日
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Slave {
}

7、定義Aspect切面(非常重要)。

關鍵點1:類上面使用了@Order註解,這是因為目標方法的@Transactional開啟了事務也是一個AOP切面,需要通過Order屬性去定義AOP切面的先後執行順序。 order越小,在AOP的chain中越靠前,越先執行(chain模式)。而事務切面的優先順序是預設最小的,所以我們需要將切換資料來源的操作放在事務前面。

關鍵點2:切面使用@Around註解而不是@Before註解,這是因為我們在目標方法執行前進行了資料來源切換,需要等目標方法執行完之後將其還原,恢復到之前的資料來源(具體看業務場景吧)。

/**
 * 資料來源的切入面
 * @author z_hh  
 * @date 2018年11月30日
 */
@Aspect
@Component
@Order(Ordered.LOWEST_PRECEDENCE - 1)
@Slf4j
public class DataSourceAOP {

	/**
	 * Master註解
	 * @param pjp
	 * @throws Throwable
	 */
	@Around("@annotation(cn.zhh.db.rw_separate.annotation.Master)")
	public Object setWriteDataSourceType(ProceedingJoinPoint pjp) throws Throwable {
		// 1、當前資料來源型別
		DatabaseType currentType = DynamicDataSource.getType();
		try {
			// 2、如果當前是Slave,就切換到Master
			if (Objects.equals(currentType, DatabaseType.SLAVE)) {
				DynamicDataSource.master();
				log.info("dataSource切換到:master");
			}
			// 3、執行目標方法
			return pjp.proceed();
		} catch (Throwable t) {
			log.error("切換資料來源異常!", t);
			throw t;
		} finally {
			// 4、需要將資料來源還原
			DynamicDataSource.setDatabaseType(currentType);
		}
	}

	/**
	 * Slave註解
	 * @param pjp
	 * @throws Throwable
	 */
	@Around("@annotation(cn.zhh.db.rw_separate.annotation.Slave) && [email protected](cn.zhh.db.rw_separate.annotation.Master)")
	public Object setReadDataSourceType(ProceedingJoinPoint pjp) throws Throwable {
		// 1、當前資料來源型別
		DatabaseType currentType = DynamicDataSource.getType();
		try {
			// 2、如果當前是Master,就切換到Slave
			if (Objects.equals(currentType, DatabaseType.MASTER)) {
				DynamicDataSource.slave();
				log.info("dataSource切換到:slave");
			}
			// 3、執行目標方法
			return pjp.proceed();
		} catch (Throwable t) {
			log.error("切換資料來源異常!", t);
			throw t;
		} finally {
			// 4、需要將資料來源還原
			DynamicDataSource.setDatabaseType(currentType);
		}
	}

}

8、在業務程式碼裡面使用自定義註解。

我們在寫方法save使用了@Master註解,在讀方法findById使用了@Slave方法。

/**
 * Student業務邏輯層
 * @author z_hh  
 * @date 2018年11月30日
 */
@Service
@Transactional
public class StudentServiceImpl implements StudentService {
	
	@Autowired
	private StudentDao dao;

	@Master
	@Override
	public Student save(Student student) {
		return dao.save(student);
	}

	@Override
	public void delete(Long id) {
		dao.deleteById(id);
	}

	@Slave
	@Override
	public Student findById(Long id) {
		return dao.findById(id).orElse(null);
	}

}

9、編寫Junit測試程式碼。

測試前我們確認了兩個資料庫id為1的兩條記錄的name屬性是不一樣的,所以測試通過的話說明我們的通過AOP切換資料來源功能生效了。

**
 * 測試類
 * @author z_hh  
 * @date 2018年11月30日
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class CommonTest {

	@Autowired
	private StudentService service;
	
	@Test
	public void test1() {
		// 儲存student1到Master庫,id=1,name=zhou
		Student student1 = Student.builder()
				.id(1L)
				.name("zhou")
				.sex(1)
				.age(24)
				.birthday(new Date())
				.build();
		assertNotNull(service.save(student1));
		
		// 從Slave庫獲取id=1的student2
		Student student2 = service.findById(1L);
		assertNotNull(student2);
		
		// 比較student1和student2的name,兩個庫裡面是不相等的
		assertNotEquals(student1.getName(), student2.getName());
	}
}

10、我們也可以通過打印出來的日誌證明。

可以優化的地方:

(1)可以僅用一個自定義註解,定義一個屬性用於設定需要使用的資料來源,對應的切面邏輯也要修改。

(2)@Around註解可以改用@Before和@After註解。@Before裡設定動態資料來源本地執行緒共享物件裡的值為想切換到的資料來源的邏輯名稱;@After裡將動態資料來源本地執行緒共享物件裡的值移除掉,即可恢復到之前的資料來源。

本文內容到此結束了,有什麼問題或者建議,歡迎在評論區進行探討!