本文目录导读:

我提供一个完整的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应用的需求,可以通过增加更多功能模块、优化性能参数来根据实际业务情况进行调整。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播