本文目录导读:

我提供一个完整的Java实现SaaS架构案例,包含多租户管理、数据隔离和计费管理等核心功能。
项目整体架构
saas-architecture/
├── pom.xml
├── src/main/java/com/example/saas/
│ ├── SaasApplication.java
│ ├── config/
│ │ ├── TenantConfig.java
│ │ ├── DatabaseConfig.java
│ │ └── SecurityConfig.java
│ ├── tenant/
│ │ ├── TenantContext.java
│ │ ├── TenantInterceptor.java
│ │ ├── TenantResolver.java
│ │ └── Tenant.java
│ ├── security/
│ │ ├── JwtTokenProvider.java
│ │ ├── UserPrincipal.java
│ │ └── CurrentUser.java
│ ├── controller/
│ │ ├── AuthController.java
│ │ ├── UserController.java
│ │ ├── OrderController.java
│ │ └── SubscriptionController.java
│ ├── service/
│ │ ├── UserService.java
│ │ ├── OrderService.java
│ │ ├── TenantService.java
│ │ └── SubscriptionService.java
│ ├── repository/
│ │ ├── UserRepository.java
│ │ ├── OrderRepository.java
│ │ └── TenantRepository.java
│ ├── entity/
│ │ ├── User.java
│ │ ├── Order.java
│ │ └── Tenant.java
│ ├── dto/
│ │ ├── LoginRequest.java
│ │ ├── LoginResponse.java
│ │ ├── OrderRequest.java
│ │ └── TenantRegistrationRequest.java
│ └── exception/
│ ├── GlobalExceptionHandler.java
│ └── CustomException.java
└── src/main/resources/
├── application.yml
└── db/
└── migration/
核心代码实现
1 多租户配置
// TenantContext.java - 租户上下文
public class TenantContext {
private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();
private static final ThreadLocal<Long> CURRENT_USER_ID = new ThreadLocal<>();
public static void setTenantId(String tenantId) {
CURRENT_TENANT.set(tenantId);
}
public static String getTenantId() {
return CURRENT_TENANT.get();
}
public static void setUserId(Long userId) {
CURRENT_USER_ID.set(userId);
}
public static Long getUserId() {
return CURRENT_USER_ID.get();
}
public static void clear() {
CURRENT_TENANT.remove();
CURRENT_USER_ID.remove();
}
}
// TenantInterceptor.java - 租户拦截器
@Component
public class TenantInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 白名单路径直接放行
String path = request.getRequestURI();
if (path.contains("/api/auth/") || path.contains("/api/tenants/register")) {
return true;
}
// 从Header获取租户信息
String tenantId = request.getHeader("X-Tenant-ID");
String token = request.getHeader("Authorization");
if (tenantId == null && token != null) {
// 从JWT中提取租户信息
tenantId = extractTenantFromToken(token);
}
if (tenantId == null) {
throw new CustomException("缺少租户信息", HttpStatus.BAD_REQUEST);
}
// 验证租户是否存在且有效
if (!validateTenant(tenantId)) {
throw new CustomException("租户不存在或已过期", HttpStatus.UNAUTHORIZED);
}
TenantContext.setTenantId(tenantId);
// 动态切换数据源
DynamicDataSource.setCurrentDataSource(tenantId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
TenantContext.clear();
DynamicDataSource.clear();
}
}
// TenantConfig.java - 租户配置
@Configuration
@ConfigurationProperties(prefix = "saas")
public class TenantConfig {
private List<TenantProperties> tenants = new ArrayList<>();
private int maxTenants;
private boolean allowSelfRegistration;
@Data
public static class TenantProperties {
private String id;
private String name;
private String database;
private String schema;
private String status;
private Integer userLimit;
private Integer storageLimit;
}
public Optional<TenantProperties> getTenant(String tenantId) {
return tenants.stream()
.filter(t -> t.getId().equals(tenantId))
.findFirst();
}
}
2 动态数据源实现
// DynamicDataSource.java - 动态数据源
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> CURRENT_DATA_SOURCE = new ThreadLocal<>();
@Autowired
private DataSourceConfig dataSourceConfig;
@Override
protected Object determineCurrentLookupKey() {
return CURRENT_DATA_SOURCE.get();
}
public static void setCurrentDataSource(String tenantId) {
CURRENT_DATA_SOURCE.set("tenant_" + tenantId);
}
public static void clear() {
CURRENT_DATA_SOURCE.remove();
}
@Override
public void afterPropertiesSet() {
// 初始化默认数据源
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("default", dataSourceConfig.getDefaultDataSource());
// 动态注册租户数据源
for (Tenant tenant : tenantRepository.findAll()) {
DataSource dataSource = dataSourceConfig.createDataSource(tenant);
targetDataSources.put("tenant_" + tenant.getId(), dataSource);
}
this.setDefaultTargetDataSource(dataSourceConfig.getDefaultDataSource());
this.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
}
// DatabaseConfig.java - 数据库配置
@Configuration
public class DatabaseConfig {
@Bean
public DataSource defaultDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/saas_platform");
config.setUsername("saas_admin");
config.setPassword("secret");
config.setMaximumPoolSize(20);
config.setConnectionTimeout(30000);
return new HikariDataSource(config);
}
public DataSource createDataSource(Tenant tenant) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(tenant.getDatabaseUrl());
config.setUsername(tenant.getDatabaseUsername());
config.setPassword(tenant.getDatabasePassword());
config.setMaximumPoolSize(10);
config.setConnectionTimeout(30000);
// 设置连接池名称便于监控
config.setPoolName("tenant-" + tenant.getId());
return new HikariDataSource(config);
}
@Bean
@Primary
public DynamicDataSource dynamicDataSource() {
return new DynamicDataSource();
}
@Bean
@Primary
public JdbcTemplate jdbcTemplate(@Qualifier("dynamicDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
3 实体类设计
// Tenant.java
@Entity
@Table(name = "tenants")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Tenant {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String tenantId;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String plan;
@Column(nullable = false)
private String status;
private String databaseUrl;
private String databaseUsername;
private String databasePassword;
@Column(nullable = false)
private Integer maxUsers;
@Column(nullable = false)
private Integer maxStorage;
@Column(nullable = false)
private Integer currentUsers;
private LocalDateTime expiryDate;
private LocalDateTime createdAt;
@OneToMany(mappedBy = "tenant", cascade = CascadeType.ALL)
private List<TenantUser> users;
}
// User.java
@Entity
@Table(name = "users")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String fullName;
@Column(nullable = false)
private String role;
private boolean enabled;
private LocalDateTime lastLogin;
// 租户级应用
@Column(name = "tenant_id")
private String tenantId;
@Version
private Integer version;
}
// Order.java - 业务实体
@Entity
@Table(name = "orders")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String orderNumber;
@Column(nullable = false)
private Long customerId;
@Column(nullable = false)
private BigDecimal totalAmount;
@Column(nullable = false)
private String status;
private String tenantId;
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> items;
}
4 多租户Repository实现
// TenantAwareRepository.java - 租户感知的Repository基类
@NoRepositoryBean
public interface TenantAwareRepository<T, ID> extends JpaRepository<T, ID> {
// 自动注入租户过滤条件
@Query("select e from #{#entityName} e where e.tenantId = :tenantId")
List<T> findByTenantId(@Param("tenantId") String tenantId);
// 带分页查询
@Query("select e from #{#entityName} e where e.tenantId = :tenantId")
Page<T> findByTenantId(@Param("tenantId") String tenantId, Pageable pageable);
// 计数
@Query("select count(e) from #{#entityName} e where e.tenantId = :tenantId")
long countByTenantId(@Param("tenantId") String tenantId);
}
// UserRepository.java
@Repository
public interface UserRepository extends TenantAwareRepository<User, Long> {
Optional<User> findByUsernameAndTenantId(String username, String tenantId);
Optional<User> findByEmailAndTenantId(String email, String tenantId);
List<User> findByTenantIdAndRole(String tenantId, String role);
@Query("select u from User u where u.tenantId = :tenantId and " +
"(u.username like %:keyword% or u.fullName like %:keyword%)")
Page<User> searchUsers(@Param("tenantId") String tenantId,
@Param("keyword") String keyword,
Pageable pageable);
}
// OrderRepository.java
@Repository
public interface OrderRepository extends TenantAwareRepository<Order, Long> {
Optional<Order> findByOrderNumberAndTenantId(String orderNumber, String tenantId);
List<Order> findByCustomerIdAndTenantId(Long customerId, String tenantId);
@Query("select sum(o.totalAmount) from Order o where o.tenantId = :tenantId and " +
"o.status = :status")
BigDecimal sumTotalAmountByStatus(@Param("tenantId") String tenantId,
@Param("status") String status);
}
5 服务层实现
// TenantService.java
@Service
@Slf4j
public class TenantService {
@Autowired
private TenantRepository tenantRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private TenantManagementClient tenantManagementClient;
public Tenant registerTenant(TenantRegistrationRequest request) {
// 检查租户ID是否唯一
if (tenantRepository.existsByTenantId(request.getTenantId())) {
throw new CustomException("租户ID已存在", HttpStatus.CONFLICT);
}
// 创建租户
Tenant tenant = Tenant.builder()
.tenantId(request.getTenantId())
.name(request.getCompanyName())
.plan(request.getPlan() != null ? request.getPlan() : "free")
.status("active")
.maxUsers(calculateMaxUsers(request.getPlan()))
.maxStorage(calculateMaxStorage(request.getPlan()))
.currentUsers(0)
.expiryDate(LocalDateTime.now().plusDays(30))
.createdAt(LocalDateTime.now())
.build();
// 创建数据库和用户
tenantManagementClient.createTenantDatabase(tenant);
// 创建默认管理员
createDefaultAdmin(tenant, request);
// 保存租户信息
tenant = tenantRepository.save(tenant);
log.info("New tenant registered: {}", tenant.getTenantId());
return tenant;
}
private void createDefaultAdmin(Tenant tenant, TenantRegistrationRequest request) {
// 在独立事务中执行,避免影响主租户
TenantContext.setTenantId(tenant.getTenantId());
User admin = User.builder()
.username("admin")
.password(passwordEncoder.encode(request.getAdminPassword()))
.email(request.getAdminEmail())
.fullName("System Administrator")
.role("ROLE_ADMIN")
.tenantId(tenant.getTenantId())
.enabled(true)
.build();
try {
userRepository.save(admin);
} finally {
TenantContext.clear();
}
}
private int calculateMaxUsers(String plan) {
switch (plan) {
case "free": return 10;
case "basic": return 50;
case "pro": return 200;
case "enterprise": return Integer.MAX_VALUE;
default: return 10;
}
}
}
// UserService.java
@Service
@Slf4j
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private TenantLimiterService tenantLimiterService;
public User createUser(RegisterUserRequest request) {
String tenantId = TenantContext.getTenantId();
// 检查租户用户数限制
tenantLimiterService.checkUserLimit(tenantId);
// 检查用户名唯一性
if (userRepository.findByUsernameAndTenantId(request.getUsername(), tenantId).isPresent()) {
throw new CustomException("用户名已存在", HttpStatus.CONFLICT);
}
User user = User.builder()
.username(request.getUsername())
.password(passwordEncoder.encode(request.getPassword()))
.email(request.getEmail())
.fullName(request.getFullName())
.role("ROLE_USER")
.tenantId(tenantId)
.enabled(true)
.build();
user = userRepository.save(user);
// 更新租户用户计数
tenantLimiterService.updateUserCount(tenantId, 1);
log.info("User created: {} in tenant: {}", user.getUsername(), tenantId);
return user;
}
public Page<User> getUsers(int page, int size) {
String tenantId = TenantContext.getTenantId();
Pageable pageable = PageRequest.of(page, size, Sort.by("createdDate").descending());
return userRepository.findByTenantId(tenantId, pageable);
}
public void deleteUser(Long userId) {
String tenantId = TenantContext.getTenantId();
User user = userRepository.findByIdAndTenantId(userId, tenantId)
.orElseThrow(() -> new CustomException("用户不存在", HttpStatus.NOT_FOUND));
userRepository.delete(user);
// 更新租户用户计数
tenantLimiterService.updateUserCount(tenantId, -1);
}
}
// OrderService.java
@Service
@Slf4j
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private OrderItemRepository orderItemRepository;
@Autowired
private TenantLimiterService tenantLimiterService;
@Transactional
public Order createOrder(CreateOrderRequest request) {
String tenantId = TenantContext.getTenantId();
Long userId = TenantContext.getUserId();
// 检查租户限额
tenantLimiterService.checkOrderLimit(tenantId);
// 创建订单
Order order = Order.builder()
.orderNumber(generateOrderNumber())
.customerId(request.getCustomerId())
.totalAmount(BigDecimal.ZERO)
.status("PENDING")
.tenantId(tenantId)
.build();
order = orderRepository.save(order);
// 添加订单项
BigDecimal total = BigDecimal.ZERO;
for (OrderItemRequest itemRequest : request.getItems()) {
OrderItem item = OrderItem.builder()
.orderId(order.getId())
.productId(itemRequest.getProductId())
.quantity(itemRequest.getQuantity())
.price(itemRequest.getPrice())
.tenantId(tenantId)
.build();
item = orderItemRepository.save(item);
total = total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())));
order.getItems().add(item);
}
// 更新订单总额
order.setTotalAmount(total);
order = orderRepository.save(order);
log.info("Order created: {} in tenant: {}", order.getOrderNumber(), tenantId);
return order;
}
@Transactional
public void processOrder(Long orderId) {
String tenantId = TenantContext.getTenantId();
Order order = orderRepository.findByIdAndTenantId(orderId, tenantId)
.orElseThrow(() -> new CustomException("订单不存在", HttpStatus.NOT_FOUND));
// 处理订单逻辑
order.setStatus("PROCESSING");
// 发送消息队列通知
kafkaTemplate.send("order-processing", order.getId());
orderRepository.save(order);
}
}
6 租户限额和计费管理
// TenantLimiterService.java
@Service
public class TenantLimiterService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private TenantRepository tenantRepository;
private final Map<String, Map<String, Integer>> quotas = new ConcurrentHashMap<>();
public void checkUserLimit(String tenantId) {
// 查询缓存
String key = "tenant:" + tenantId + ":users";
String count = redisTemplate.opsForValue().get(key);
if (count != null) {
int currentCount = Integer.parseInt(count);
Tenant tenant = tenantRepository.findByTenantId(tenantId).orElseThrow();
if (currentCount >= tenant.getMaxUsers()) {
throw new CustomException("达到租户用户数上限", HttpStatus.BAD_REQUEST);
}
}
}
public void updateUserCount(String tenantId, int delta) {
String key = "tenant:" + tenantId + ":users";
redisTemplate.opsForValue().increment(key, delta);
}
public void checkStorage(String tenantId, long fileSize) {
Tenant tenant = tenantRepository.findByTenantId(tenantId).orElseThrow();
long currentUsage = getStorageUsage(tenantId);
long newUsage = currentUsage + fileSize;
if (newUsage > tenant.getMaxStorage() * 1024L * 1024L) {
throw new CustomException("超出存储配额", HttpStatus.BAD_REQUEST);
}
}
public void checkOrderLimit(String tenantId) {
// 检查每日订单数
String today = LocalDate.now().toString();
String key = "tenant:" + tenantId + ":orders:" + today;
long count = redisTemplate.opsForValue().increment(key);
if (count > getDailyOrderLimit(tenantId)) {
throw new CustomException("超出每日订单限额", HttpStatus.BAD_REQUEST);
}
// 设置过期时间(24小时后)
redisTemplate.expire(key, 24, TimeUnit.HOURS);
}
private int getDailyOrderLimit(String tenantId) {
Tenant tenant = tenantRepository.findByTenantId(tenantId).orElseThrow();
switch (tenant.getPlan()) {
case "free": return 100;
case "basic": return 1000;
case "pro": return 10000;
case "enterprise": return Integer.MAX_VALUE;
default: return 100;
}
}
}
// SubscriptionService.java - 订阅管理
@Service
@Slf4j
public class SubscriptionService {
@Autowired
private TenantRepository tenantRepository;
@Autowired
private PaymentService paymentService;
public SubscriptionPlan upgradePlan(String tenantId, NewPlanRequest request) {
Tenant tenant = tenantRepository.findByTenantId(tenantId).orElseThrow();
// 计算费用
BigDecimal amount = calculateUpgradeCost(tenant.getPlan(), request.getNewPlan());
// 处理支付
PaymentResult payment = paymentService.processPayment(tenant, amount, request.getPaymentMethod());
// 更新租户计划
tenant.setPlan(request.getNewPlan());
tenant.setMaxUsers(calculateMaxUsers(request.getNewPlan()));
tenantRepository.save(tenant);
// 更新配额
updateQuotas(tenant);
log.info("Tenant {} upgraded to plan: {}", tenantId, request.getNewPlan());
return SubscriptionPlan.builder()
.tenantId(tenantId)
.plan(request.getNewPlan())
.amount(amount)
.effectiveDate(LocalDateTime.now())
.build();
}
public void renewSubscription(String tenantId) {
Tenant tenant = tenantRepository.findByTenantId(tenantId).orElseThrow();
BigDecimal amount = calculateRenewalCost(tenant.getPlan());
PaymentResult payment = paymentService.processPayment(tenant, amount, null);
// 续期30天
tenant.setExpiryDate(tenant.getExpiryDate().plusDays(30));
tenantRepository.save(tenant);
}
}
7 安全配置
// SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private RestAuthenticationEntryPoint authenticationEntryPoint;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors().and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/tenants/register").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint);
// 添加JWT过滤器
http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, userDetailsService),
UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
// JwtTokenProvider.java
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private Long jwtExpiration;
public String generateToken(UserPrincipal userPrincipal) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpiration);
return Jwts.builder()
.setSubject(userPrincipal.getUsername())
.claim("userId", userPrincipal.getId())
.claim("tenantId", userPrincipal.getTenantId())
.claim("role", userPrincipal.getRole())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public Long getUserIdFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.get("userId").toString());
}
public String getTenantFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return claims.get("tenantId").toString();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
throw new CustomException("无效的JWT令牌", HttpStatus.UNAUTHORIZED);
}
}
}
8 Controller层
// AuthController.java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserService userService;
@PostMapping("/login")
public LoginResponse login(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
String token = tokenProvider.generateToken(userPrincipal);
TenantContext.setTenantId(userPrincipal.getTenantId());
TenantContext.setUserId(userPrincipal.getId());
return LoginResponse.builder()
.token(token)
.tokenType("Bearer")
.expiresIn(3600)
.user(generateUserInfo(userPrincipal))
.build();
}
@PostMapping("/refresh")
public TokenResponse refreshToken(@Valid @RequestBody RefreshTokenRequest request) {
// token刷新逻辑
}
}
// OrderController.java
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping
@PreAuthorize("hasRole('ADMIN') or hasRole('MANAGER')")
public ResponseEntity<Order> createOrder(@Valid @RequestBody CreateOrderRequest request) {
Order order = orderService.createOrder(request);
return ResponseEntity.ok(order);
}
@GetMapping("/{orderId}")
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER', 'USER')")
public Order getOrder(@PathVariable Long orderId) {
return orderService.getOrder(orderId);
}
@GetMapping
public Page<Order> getOrders(
@RequestParam(required = false) String status,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return orderService.getOrders(status, page, size);
}
@PatchMapping("/{orderId}/cancel")
@PreAuthorize("hasRole('ADMIN')")
public Order cancelOrder(@PathVariable Long orderId) {
return orderService.cancelOrder(orderId);
}
}
9 异常处理和配置文件
// GlobalExceptionHandler.java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiError handleValidationException(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return new ApiError(HttpStatus.BAD_REQUEST.value(), "Validation failed", errors);
}
@ExceptionHandler(CustomException.class)
public ResponseEntity<ApiError> handleCustomException(CustomException ex) {
return ResponseEntity.status(ex.getStatus())
.body(new ApiError(ex.getStatus().value(), ex.getMessage()));
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiError handleGenericException(Exception ex) {
log.error("Unexpected error", ex);
return new ApiError(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Internal server error");
}
}
10 配置文件
# application.yml
spring:
application:
name: saas-architecture
datasource:
platform-url: jdbc:mysql://localhost:3306/saas_platform
platform-username: saas_admin
platform-password: ${PLATFORM_DB_PASSWORD}
redis:
host: localhost
port: 6379
timeout: 5000
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
kafka:
bootstrap-servers: localhost:9092
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
consumer:
group-id: saas-group
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
saas:
allow-self-registration: true
max-tenants: 100
tenants:
- id: t001
name: "Tenant A"
database: saas_tenant_a
status: active
- id: t002
name: "Tenant B"
database: saas_tenant_b
status: active
jwt:
secret: ${JWT_SECRET:your-secret-key-should-be-long-and-secure}
expiration: 86400000 # 24小时
server:
port: 8080
servlet:
context-path: /api
logging:
level:
com.example.saas: DEBUG
测试和验证
// 集成测试示例
@SpringBootTest
@AutoConfigureMockMvc
public class SaasServiceTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
public void testTenantRegistration() throws Exception {
// 1. 租户注册
TenantRegistrationRequest request = TenantRegistrationRequest.builder()
.tenantId("test-tenant")
.companyName("Test Company")
.adminEmail("admin@test.com")
.adminPassword("password123")
.plan("basic")
.build();
mockMvc.perform(post("/api/tenants/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk());
// 2. 租户登录
LoginRequest loginRequest = LoginRequest.builder()
.username("admin")
.password("password123")
.build();
mockMvc.perform(post("/api/auth/login")
.header("X-Tenant-ID", "test-tenant")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token").exists());
}
@Test
public void testMultiTenantDataIsolation() throws Exception {
// 验证不同租户之间的数据隔离
String tenantA = "tenant-a";
String tenantB = "tenant-b";
// 创建两个租户的数据
createOrder(tenantA, "ORDER-001");
createOrder(tenantB, "ORDER-002");
// 查询租户A的订单,不应包含租户B的数据
mockMvc.perform(get("/api/orders")
.header("Authorization", getToken(tenantA))
.header("X-Tenant-ID", tenantA))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content[0].orderNumber").value("ORDER-001"))
.andExpect(jsonPath("$.content", hasSize(1)));
}
}
关键特性总结
- 多租户架构:支持数据库级隔离,每个租户独立数据库
- 动态数据源:根据租户上下文动态切换数据库连接
- 安全认证:JWT+租户上下文双重验证
- 租户管理:完整的租户生命周期管理
- 限额控制:基于Redis的实时配额管理
- 计费管理:支持订阅计划升级和续费
- 数据隔离:确保租户间数据安全隔离
- 可扩展性:支持横向扩展和性能优化
这个架构实现了完整的SaaS多租户支持,能够满足大多数企业级SaaS应用的需求,可以通过增加更多功能模块、优化性能参数来根据实际业务情况进行调整。