闽公网安备 35020302035485号
在现代应用开发中,多租户架构日益重要,如何为每个租户提供独立数据库,保障数据隔离与安全。本文将详述如何基于Spring Boot和MySQL构建此架构应用,从环境搭建到核心功能实现,助力开发者快速上手。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
二.数据库连接与租户数据源配置import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DataSourceConfig {
private static final String MYSQL_DRIVER_CLASS_NAME = "com.mysql.cj.jdbc.Driver";
private static final String DEFAULT_DB_URL = "jdbc:mysql://localhost:3306/default_tenant_db?useSSL=false&serverTimezone=UTC";
private static final String DEFAULT_USERNAME = "root";
private static final String DEFAULT_PASSWORD = "password";
// 默认数据源
@Bean
public DataSource defaultDataSource() {
return DataSourceBuilder.create()
.driverClassName(MYSQL_DRIVER_CLASS_NAME)
.url(DEFAULT_DB_URL)
.username(DEFAULT_USERNAME)
.password(DEFAULT_PASSWORD)
.build();
}
// 租户数据源
@Bean
public Map<String, DataSource> tenantDataSources() {
Map<String, DataSource> dataSourceMap = new HashMap<>();
// 假设从配置文件或其他数据源获取租户信息,此处简化为硬编码示例
String tenant1DbUrl = "jdbc:mysql://localhost:3306/tenant1_db?useSSL=false&serverTimezone=UTC";
String tenant1Username = "tenant1_user";
String tenant1Password = "tenant1_password";
DataSource tenant1DataSource = DataSourceBuilder.create()
.driverClassName(MYSQL_DRIVER_CLASS_NAME)
.url(tenant1DbUrl)
.username(tenant1Username)
.password(tenant1Password)
.build();
dataSourceMap.put("tenant1", tenant1DataSource);
// 可继续添加更多租户数据源
return dataSourceMap;
}
// 堆代码 duidaima.com
// 路由数据源
@Bean
public DataSource routingDataSource(Map<String, DataSource> tenantDataSources, DataSource defaultDataSource) {
CustomRoutingDataSource routingDataSource = new CustomRoutingDataSource();
routingDataSource.setDefaultTargetDataSource(defaultDataSource);
routingDataSource.setTargetDataSources(tenantDataSources);
routingDataSource.afterPropertiesSet();
return routingDataSource;
}
}
其中,CustomRoutingDataSource继承自AbstractRoutingDataSource,用于根据租户标识动态切换数据源:import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class CustomRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TenantContext.getCurrentTenant();
}
}
TenantContext类用于管理当前租户信息:public class TenantContext {
private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
public static void setCurrentTenant(String tenant) {
currentTenant.set(tenant);
}
public static String getCurrentTenant() {
return currentTenant.get();
}
public static void clear() {
currentTenant.remove();
}
}
三.租户识别与请求拦截import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class TenantInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String tenantId = request.getHeader("X-Tenant-Id");
if (tenantId!= null) {
TenantContext.setCurrentTenant(tenantId);
}
returntrue;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
TenantContext.clear();
}
}
在WebMvcConfig类中注册拦截器: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 WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TenantInterceptor()).addPathPatterns("/**");
}
}
四、数据实体与持久层操作import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// 省略构造函数、getter 和 setter 方法
}
创建CustomerRepository接口继承自JpaRepository进行数据持久化操作:import org.springframework.data.jpa.repository.JpaRepository;
public interface CustomerRepository extends JpaRepository<Customer, Long> {
}
五、业务逻辑层与 API 实现import org.springframework.stereotype.Service;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
@Service
public class CustomerService {
@PersistenceContext
private EntityManager entityManager;
public List<Customer> getCustomers() {
String tenant = TenantContext.getCurrentTenant();
String jpql = "SELECT c FROM Customer c";
return entityManager.createQuery(jpql, Customer.class).getResultList();
}
}
创建CustomerController类提供API接口:import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class CustomerController {
private final CustomerService customerService;
public CustomerController(CustomerService customerService) {
this.customerService = customerService;
}
@GetMapping("/customers")
public List<Customer> getCustomers() {
return customerService.getCustomers();
}
}
六、测试与验证