在现代应用开发中,多租户架构日益重要,如何为每个租户提供独立数据库,保障数据隔离与安全。本文将详述如何基于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(); } }六、测试与验证