Spring Boot 使用 AbstractRoutingDataSource实现动态数据源路由

x33g5p2x  于2021-10-16 转载在 Spring  
字(12.2k)|赞(0)|评价(0)|浏览(502)

本页将在 Spring Boot 中使用 AbstractRoutingDataSource 和 Spring Data JPA 完成动态数据源路由。 AbstractRoutingDataSourceDataSource 的抽象实现,它根据查找键将调用路由到各种目标数据源之一。

AbstractRoutingDataSource 在 Spring 的 2.0.1 版本 中引入,提供了一种根据当前上下文动态确定实际数据源的方法。它维护通过更改 context 切换的多个数据源的 map
类似帖子:使用 Spring JPA 的 Spring Boot 多数据源示例

有时我们需要根据地域或语言动态切换数据库,根据请求进行操作。

假设,我们有两个分支机构,即 BangkokHongkong,每个分支机构有一个数据库。如果请求来自 Bangkok 分支,我们需要从 Bangkok 数据库获取详细信息,如果请求来自 Hongkong 分支,则来自 Hongkong 数据库。

1. 我们将构建什么

在本教程中,我们将公开一个连接 hongkongdbbangkokdb 的 REST 端点,并根据请求从表中获取员工信息并返回 JSON 对象。

端点:http://localhost:8080/employee

1.1 如果 branch =hongkong 那么它将连接 hongkongdb 并获取员工信息并返回:

[
    {
        "id": 5,
        "name": "Jackie Chan",
        "branch": "hongkong"
    },
    {
        "id": 8,
        "name": "Maggie Cheung",
        "branch": "hongkong"
    }
]

1.2 同理 if branch =bangkok 然后连接bangkokdb,获取曼谷分行的员工信息并返回:

[
    {
        "id": 1,
        "name": "Tony Jaa",
        "branch": "bangkok"
    }
]

注意: 在上面的请求中,我们添加了一个名为 “branch”请求标头,第一个请求的值为 Bangkok,第二个请求为 Hongkong。

2. 我们需要什么

  • 约 30 分钟
  • JDK 1.8 或更高版本
  • Spring Boot 2.2.2.RELEASE
  • Spring Data JPA 2.2.3.RELEASE
  • Gradle 4+ 或 Maven 3.2+
  • MySQL 数据库

3. 需要依赖

将以下依赖项添加到 pom.xml 文件中。
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.2.RELEASE</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<groupId>org.websparrow</groupId>
	<artifactId>spring-boot-datasource-routing</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>
	</dependencies>

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

</project>

4. 项目结构

我们在 STS 4 IDE 中的应用程序的最终项目结构如下所示:

5.application.properties

application.properties 文件中为两个数据源(即 hongkongdbbangkokdb)配置数据库(数据源)连接字符串。
application.properties

#database details for bangkok branch
bangkok.datasource.url=jdbc:mysql://localhost:3306/bangkokdb?createDatabaseIfNotExist=true
bangkok.datasource.username=root
bangkok.datasource.password=root

#database details for hongkong branch
hongkong.datasource.url=jdbc:mysql://localhost:3306/hongkongdb?createDatabaseIfNotExist=true
hongkong.datasource.username=root
hongkong.datasource.password=root

# JPA property settings
spring.jpa.database=mysql
spring.jpa.hibernate.ddl-auto=update
spring.jpa.generate-ddl=true
spring.jpa.show-sql=true

6. 实体

首先——让我们创建一个简单的实体,它将存在于两个数据库中。
Employee.java

package org.websparrow.entity;

@Entity
@Table(name = "employee")
public class Employee {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	private String name;
	private String branch;
	// Generate Getters and Setters...
}

7. 数据源上下文

我们创建了一个 BranchEnum 来保存两个分支的名称。 AbstractRoutingDataSource 需要一条信息到哪个数据库路由。这里这个枚举将作为 AbstractRoutingDataSource 类的上下文。
BranchEnum.java

package org.websparrow.constant;

public enum BranchEnum {

	BANGKOK, HONGKONG
}

8. 上下文持有者

BranchContextHolder 将保存分支的当前上下文作为 ThreadLocal 引用。这个类将提供对BranchEnum线程绑定访问。
BranchContextHolder.java

package org.websparrow.config;

import org.websparrow.constant.BranchEnum;

public class BranchContextHolder {

	private static ThreadLocal<BranchEnum> threadLocal = new ThreadLocal<>();

	public static void setBranchContext(BranchEnum branchEnum) {
		threadLocal.set(branchEnum);
	}

	public static BranchEnum getBranchContext() {
		return threadLocal.get();
	}

	public static void clearBranchContext() {
		threadLocal.remove();
	}
}

9. 数据源路由

DataSourceRouting 扩展了 AbstractRoutingDatasource 类,该类将包含真实数据源的映射。覆盖其 determineCurrentLookupKey() 方法以确定当前的查找键。这通常用于检查线程绑定事务上下文
DataSourceRouting.java

package org.websparrow.config;

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

import javax.sql.DataSource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.websparrow.constant.BranchEnum;

public class DataSourceRouting extends AbstractRoutingDataSource {

	@Override
	protected Object determineCurrentLookupKey() {
		return BranchContextHolder.getBranchContext();
	}

	public void initDatasource(DataSource bangkokDataSource,
			DataSource hongkongDataSource) {
		Map<Object, Object> dataSourceMap = new HashMap<>();
		dataSourceMap.put(BranchEnum.BANGKOK, bangkokDataSource);
		dataSourceMap.put(BranchEnum.HONGKONG, hongkongDataSource);
		this.setTargetDataSources(dataSourceMap);
		this.setDefaultTargetDataSource(bangkokDataSource);
	}
}

数据源映射设置为targetDataSources,并选择一个数据源作为默认数据源。

10. 数据源配置细节

我们将创建两个类来保存两个数据库的数据库连接属性。
HongkongDetails.java

package org.websparrow.model;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "hongkong.datasource")
public class HongkongDetails {

	private String url;
	private String password;
	private String username;
	// Generates Getters and Setters...
}

曼谷详情.java

package org.websparrow.model;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "bangkok.datasource")
public class BangkokDetails {

	private String url;
	private String password;
	private String username;
	// Generates Getters and Setters...
}

11. 数据源配置

现在我们将为我们的两个数据库创建数据源,将它们放入一个映射中并将其提供给 DataSourceRouting
DataSourceConfig.java

package org.websparrow.config;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.websparrow.entity.Employee;
import org.websparrow.model.BangkokDetails;
import org.websparrow.model.HongkongDetails;

@Configuration
@EnableJpaRepositories(
		basePackages = "org.websparrow.repo",
		transactionManagerRef = "transcationManager",
		entityManagerFactoryRef = "entityManager")
@EnableTransactionManagement
public class DataSourceConfig {

	@Autowired
	private BangkokDetails bangkokDetails;
	@Autowired
	private HongkongDetails hongkongDetails;

	@Bean
	@Primary
	@Autowired
	public DataSource dataSource() {
		DataSourceRouting routingDataSource = new DataSourceRouting();
		routingDataSource.initDatasource(bangkokDataSource(),
				hongkongDataSource());
		return routingDataSource;
	}

	public DataSource hongkongDataSource() {
		DriverManagerDataSource dataSource = new DriverManagerDataSource();
		dataSource.setUrl(hongkongDetails.getUrl());
		dataSource.setUsername(hongkongDetails.getUsername());
		dataSource.setPassword(hongkongDetails.getPassword());
		return dataSource;
	}

	public DataSource bangkokDataSource() {
		DriverManagerDataSource dataSource = new DriverManagerDataSource();
		dataSource.setUrl(bangkokDetails.getUrl());
		dataSource.setUsername(bangkokDetails.getUsername());
		dataSource.setPassword(bangkokDetails.getPassword());
		return dataSource;
	}

	@Bean(name = "entityManager")
	public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(
			EntityManagerFactoryBuilder builder) {
		return builder.dataSource(dataSource()).packages(Employee.class)
				.build();
	}

	@Bean(name = "transcationManager")
	public JpaTransactionManager transactionManager(
			@Autowired @Qualifier("entityManager") LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
		return new JpaTransactionManager(entityManagerFactoryBean.getObject());
	}
}

12. 数据源拦截器

DataSourceInterceptor 拦截每个请求并从请求头中获取分支信息并将其放入我们已经创建的上下文持有者中,我们将通过 BranchContextHolder 切换数据源。
DataSourceInterceptor.java

package org.websparrow.config;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.websparrow.constant.BranchEnum;

@Component
public class DataSourceInterceptor extends HandlerInterceptorAdapter {

	@Override
	public boolean preHandle(HttpServletRequest request,
			HttpServletResponse response, Object handler) throws Exception {

		String branch = request.getHeader("branch");
		if (BranchEnum.BANGKOK.toString().equalsIgnoreCase(branch))
			BranchContextHolder.setBranchContext(BranchEnum.BANGKOK);
		else
			BranchContextHolder.setBranchContext(BranchEnum.HONGKONG);
		return super.preHandle(request, response, handler);
	}
}

将此拦截器注册到 WebMvcConfigurer

WebConfig.java

package org.websparrow.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

	@Autowired
	private DataSourceInterceptor dataSourceInterceptor;

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(dataSourceInterceptor).addPathPatterns("/**");
		WebMvcConfigurer.super.addInterceptors(registry);
	}
}

13. 存储库和服务

EmployeeRepository.java

package org.websparrow.repo;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.websparrow.entity.Employee;

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {}

EmployeeService.java

package org.websparrow.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.websparrow.entity.Employee;
import org.websparrow.repo.EmployeeRepository;

@Service
public class EmployeeService {

	@Autowired
	private EmployeeRepository employeeRepository;

	public List<Employee> getEmployees() {
		return employeeRepository.findAll();
	}
}

14. 控制器

EmployeeController 类为应用程序用户公开 REST 端点。在这个控制器类中,我们创建了一个 REST 端点,如下所示:

http://localhost:8080/employee:将从选定的数据源中检索员工的记录。
EmployeeController.java

package org.websparrow.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.websparrow.entity.Employee;
import org.websparrow.service.EmployeeService;

@RestController
public class EmployeeController {

	@Autowired
	private EmployeeService employeeService;

	@GetMapping(value = "employee")
	public ResponseEntity<List<Employee>> getEmployees() {
		return ResponseEntity.status(HttpStatus.ACCEPTED)
				.body(employeeService.getEmployees());
	}
}

15. 运行应用程序

DataSourceRoutingApp 类包含主要方法并负责启动应用程序。
DataSourceRoutingApp.java

package org.websparrow;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DataSourceRoutingApp {

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

16. 测试应用程序

要测试应用程序,请通过执行上述类来启动 Spring Boot 应用程序,并使用标头参数(即分支及其值)点击以下 API:

API:http://localhost:8080/employee

16.1 如果 branch =hongkong 那么它将连接到 hongkongdb

相关文章