欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringBoot實現(xiàn)多租戶系統(tǒng)架構的5種設計方案介紹

 更新時間:2025年05月30日 08:09:58   作者:風象南  
多租戶(Multi-tenancy)是一種軟件架構模式,允許單個應用實例服務于多個客戶(租戶),同時保持租戶數(shù)據(jù)的隔離性和安全性,本文分享了SpringBoot環(huán)境下實現(xiàn)多租戶系統(tǒng)的5種架構設計方案,需要的可以參考一下

多租戶(Multi-tenancy)是一種軟件架構模式,允許單個應用實例服務于多個客戶(租戶),同時保持租戶數(shù)據(jù)的隔離性和安全性。

通過合理的多租戶設計,企業(yè)可以顯著降低運維成本、提升資源利用率,并實現(xiàn)更高效的服務交付。

本文將分享SpringBoot環(huán)境下實現(xiàn)多租戶系統(tǒng)的5種架構設計方案

方案一:獨立數(shù)據(jù)庫模式

原理與特點

獨立數(shù)據(jù)庫模式為每個租戶提供完全獨立的數(shù)據(jù)庫實例,是隔離級別最高的多租戶方案。在這種模式下,租戶數(shù)據(jù)完全分離,甚至可以部署在不同的服務器上。

實現(xiàn)步驟

1. 創(chuàng)建多數(shù)據(jù)源配置:為每個租戶配置獨立的數(shù)據(jù)源

@Configuration
public class MultiTenantDatabaseConfig {
    
    @Autowired
    private TenantDataSourceProperties properties;
    
    @Bean
    public DataSource dataSource() {
        AbstractRoutingDataSource multiTenantDataSource = new TenantAwareRoutingDataSource();
        
        Map<Object, Object> targetDataSources = new HashMap<>();
        
        // 為每個租戶創(chuàng)建數(shù)據(jù)源
        for (TenantDataSourceProperties.TenantProperties tenant : properties.getTenants()) {
            DataSource tenantDataSource = createDataSource(tenant);
            targetDataSources.put(tenant.getTenantId(), tenantDataSource);
        }
        
        multiTenantDataSource.setTargetDataSources(targetDataSources);
        return multiTenantDataSource;
    }
    
    private DataSource createDataSource(TenantDataSourceProperties.TenantProperties tenant) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(tenant.getUrl());
        dataSource.setUsername(tenant.getUsername());
        dataSource.setPassword(tenant.getPassword());
        dataSource.setDriverClassName(tenant.getDriverClassName());
        return dataSource;
    }
}

2. 實現(xiàn)租戶感知的數(shù)據(jù)源路由

public class TenantAwareRoutingDataSource extends AbstractRoutingDataSource {
    
    @Override
    protected Object determineCurrentLookupKey() {
        return TenantContextHolder.getTenantId();
    }
}

3. 租戶上下文管理

public class TenantContextHolder {
    
    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
    
    public static void setTenantId(String tenantId) {
        CONTEXT.set(tenantId);
    }
    
    public static String getTenantId() {
        return CONTEXT.get();
    }
    
    public static void clear() {
        CONTEXT.remove();
    }
}

4. 添加租戶識別攔截器

@Component
public class TenantIdentificationInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String tenantId = extractTenantId(request);
        if (tenantId != null) {
            TenantContextHolder.setTenantId(tenantId);
            return true;
        }
        
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        return false;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                                Object handler, Exception ex) {
        TenantContextHolder.clear();
    }
    
    private String extractTenantId(HttpServletRequest request) {
        // 從請求頭中獲取租戶ID
        String tenantId = request.getHeader("X-TenantID");
        
        // 或者從子域名提取
        if (tenantId == null) {
            String host = request.getServerName();
            if (host.contains(".")) {
                tenantId = host.split("\.")[0];
            }
        }
        
        return tenantId;
    }
}

5. 配置攔截器

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    private TenantIdentificationInterceptor tenantInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tenantInterceptor)
                .addPathPatterns("/api/**");
    }
}

6. 實現(xiàn)動態(tài)租戶管理

@Entity
@Table(name = "tenant")
public class Tenant {
    
    @Id
    private String id;
    
    @Column(nullable = false)
    private String name;
    
    @Column(nullable = false)
    private String databaseUrl;
    
    @Column(nullable = false)
    private String username;
    
    @Column(nullable = false)
    private String password;
    
    @Column(nullable = false)
    private String driverClassName;
    
    @Column
    private boolean active = true;
    
    // getters and setters
}

@Repository
public interface TenantRepository extends JpaRepository<Tenant, String> {
    List<Tenant> findByActive(boolean active);
}

@Service
public class TenantManagementService {
    
    @Autowired
    private TenantRepository tenantRepository;
    
    @Autowired
    private DataSource dataSource;
    
    @Autowired
    private ApplicationContext applicationContext;
    
    // 用ConcurrentHashMap存儲租戶數(shù)據(jù)源
    private final Map<String, DataSource> tenantDataSources = new ConcurrentHashMap<>();
    
    @PostConstruct
    public void initializeTenants() {
        List<Tenant> activeTenants = tenantRepository.findByActive(true);
        for (Tenant tenant : activeTenants) {
            addTenant(tenant);
        }
    }
    
    public void addTenant(Tenant tenant) {
        // 創(chuàng)建新的數(shù)據(jù)源
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(tenant.getDatabaseUrl());
        dataSource.setUsername(tenant.getUsername());
        dataSource.setPassword(tenant.getPassword());
        dataSource.setDriverClassName(tenant.getDriverClassName());
        
        // 存儲數(shù)據(jù)源
        tenantDataSources.put(tenant.getId(), dataSource);
        
        // 更新路由數(shù)據(jù)源
        updateRoutingDataSource();
        
        // 保存租戶信息到數(shù)據(jù)庫
        tenantRepository.save(tenant);
    }
    
    public void removeTenant(String tenantId) {
        DataSource dataSource = tenantDataSources.remove(tenantId);
        if (dataSource != null && dataSource instanceof HikariDataSource) {
            ((HikariDataSource) dataSource).close();
        }
        
        // 更新路由數(shù)據(jù)源
        updateRoutingDataSource();
        
        // 從數(shù)據(jù)庫移除租戶
        tenantRepository.deleteById(tenantId);
    }
    
    private void updateRoutingDataSource() {
        try {
            TenantAwareRoutingDataSource routingDataSource = (TenantAwareRoutingDataSource) dataSource;
            
            // 使用反射訪問AbstractRoutingDataSource的targetDataSources字段
            Field targetDataSourcesField = AbstractRoutingDataSource.class.getDeclaredField("targetDataSources");
            targetDataSourcesField.setAccessible(true);
            
            Map<Object, Object> targetDataSources = new HashMap<>(tenantDataSources);
            targetDataSourcesField.set(routingDataSource, targetDataSources);
            
            // 調用afterPropertiesSet初始化數(shù)據(jù)源
            routingDataSource.afterPropertiesSet();
        } catch (Exception e) {
            throw new RuntimeException("Failed to update routing data source", e);
        }
    }
}

7. 提供租戶管理API

@RestController
@RequestMapping("/admin/tenants")
public class TenantAdminController {
    
    @Autowired
    private TenantManagementService tenantService;
    
    @GetMapping
    public List<Tenant> getAllTenants() {
        return tenantService.getAllTenants();
    }
    
    @PostMapping
    public ResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {
        tenantService.addTenant(tenant);
        return ResponseEntity.status(HttpStatus.CREATED).body(tenant);
    }
    
    @DeleteMapping("/{tenantId}")
    public ResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {
        tenantService.removeTenant(tenantId);
        return ResponseEntity.noContent().build();
    }
}

優(yōu)缺點分析

優(yōu)點:

• 數(shù)據(jù)隔離級別最高,安全性最佳

• 租戶可以使用不同的數(shù)據(jù)庫版本或類型

• 易于實現(xiàn)租戶特定的數(shù)據(jù)庫優(yōu)化

• 故障隔離,一個租戶的數(shù)據(jù)庫問題不影響其他租戶

• 便于獨立備份、恢復和遷移

缺點:

• 資源利用率較低,成本較高

• 運維復雜度高,需要管理多個數(shù)據(jù)庫實例

• 跨租戶查詢困難

• 每增加一個租戶需要創(chuàng)建新的數(shù)據(jù)庫實例

• 數(shù)據(jù)庫連接池管理復雜

適用場景

高要求的企業(yè)級SaaS應用

租戶數(shù)量相對較少但數(shù)據(jù)量大的場景

租戶愿意支付更高費用獲得更好隔離性的場景

方案二:共享數(shù)據(jù)庫,獨立Schema模式

原理與特點

在這種模式下,所有租戶共享同一個數(shù)據(jù)庫實例,但每個租戶擁有自己獨立的Schema(在PostgreSQL中)或數(shù)據(jù)庫(在MySQL中)。這種方式在資源共享和數(shù)據(jù)隔離之間取得了平衡。

實現(xiàn)步驟

1. 創(chuàng)建租戶Schema配置

@Configuration
public class MultiTenantSchemaConfig {
    
    @Autowired
    private DataSource dataSource;
    
    @Autowired
    private TenantRepository tenantRepository;
    
    @PostConstruct
    public void initializeSchemas() {
        for (Tenant tenant : tenantRepository.findByActive(true)) {
            createSchemaIfNotExists(tenant.getSchemaName());
        }
    }
    
    private void createSchemaIfNotExists(String schema) {
        try (Connection connection = dataSource.getConnection()) {
            // PostgreSQL語法,MySQL使用CREATE DATABASE IF NOT EXISTS
            String sql = "CREATE SCHEMA IF NOT EXISTS " + schema;
            try (Statement stmt = connection.createStatement()) {
                stmt.execute(sql);
            }
        } catch (SQLException e) {
            throw new RuntimeException("Failed to create schema: " + schema, e);
        }
    }
}

2. 租戶實體和存儲

@Entity
@Table(name = "tenant")
public class Tenant {
    
    @Id
    private String id;
    
    @Column(nullable = false)
    private String name;
    
    @Column(nullable = false, unique = true)
    private String schemaName;
    
    @Column
    private boolean active = true;
    
    // getters and setters
}

@Repository
public interface TenantRepository extends JpaRepository<Tenant, String> {
    List<Tenant> findByActive(boolean active);
    Optional<Tenant> findBySchemaName(String schemaName);
}

3. 配置Hibernate多租戶支持

@Configuration
@EnableJpaRepositories(basePackages = "com.example.repository")
@EntityScan(basePackages = "com.example.entity")
public class JpaConfig {
    
    @Autowired
    private DataSource dataSource;
    
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            EntityManagerFactoryBuilder builder) {
        
        Map<String, Object> properties = new HashMap<>();
        properties.put(org.hibernate.cfg.Environment.MULTI_TENANT, 
                MultiTenancyStrategy.SCHEMA);
        properties.put(org.hibernate.cfg.Environment.MULTI_TENANT_CONNECTION_PROVIDER, 
                multiTenantConnectionProvider());
        properties.put(org.hibernate.cfg.Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, 
                currentTenantIdentifierResolver());
        
        // 其他Hibernate配置...
        
        return builder
                .dataSource(dataSource)
                .packages("com.example.entity")
                .properties(properties)
                .build();
    }
    
    @Bean
    public MultiTenantConnectionProvider multiTenantConnectionProvider() {
        return new SchemaBasedMultiTenantConnectionProvider();
    }
    
    @Bean
    public CurrentTenantIdentifierResolver currentTenantIdentifierResolver() {
        return new TenantSchemaIdentifierResolver();
    }
}

4. 實現(xiàn)多租戶連接提供者

public class SchemaBasedMultiTenantConnectionProvider 
        implements MultiTenantConnectionProvider {
    
    private static final long serialVersionUID = 1L;
    
    @Autowired
    private DataSource dataSource;
    
    @Override
    public Connection getAnyConnection() throws SQLException {
        return dataSource.getConnection();
    }
    
    @Override
    public void releaseAnyConnection(Connection connection) throws SQLException {
        connection.close();
    }
    
    @Override
    public Connection getConnection(String tenantIdentifier) throws SQLException {
        final Connection connection = getAnyConnection();
        try {
            // PostgreSQL語法,MySQL使用USE database_name
            connection.createStatement()
                    .execute(String.format("SET SCHEMA '%s'", tenantIdentifier));
        } catch (SQLException e) {
            throw new HibernateException("Could not alter JDBC connection to schema [" 
                    + tenantIdentifier + "]", e);
        }
        return connection;
    }
    
    @Override
    public void releaseConnection(String tenantIdentifier, Connection connection) 
            throws SQLException {
        try {
            // 恢復到默認Schema
            connection.createStatement().execute("SET SCHEMA 'public'");
        } catch (SQLException e) {
            // 忽略錯誤,確保連接關閉
        }
        connection.close();
    }
    
    @Override
    public boolean supportsAggressiveRelease() {
        return false;
    }
    
    @Override
    public boolean isUnwrappableAs(Class unwrapType) {
        return false;
    }
    
    @Override
    public <T> T unwrap(Class<T> unwrapType) {
        return null;
    }
}

5. 實現(xiàn)租戶標識解析器

public class TenantSchemaIdentifierResolver implements CurrentTenantIdentifierResolver {
    
    private static final String DEFAULT_TENANT = "public";
    
    @Override
    public String resolveCurrentTenantIdentifier() {
        String tenantId = TenantContextHolder.getTenantId();
        return tenantId != null ? tenantId : DEFAULT_TENANT;
    }
    
    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }
}

6. 動態(tài)租戶管理服務

@Service
public class TenantSchemaManagementService {
    
    @Autowired
    private TenantRepository tenantRepository;
    
    @Autowired
    private DataSource dataSource;
    
    @Autowired
    private EntityManagerFactory entityManagerFactory;
    
    public void createTenant(Tenant tenant) {
        // 1. 創(chuàng)建Schema
        createSchemaIfNotExists(tenant.getSchemaName());
        
        // 2. 保存租戶信息
        tenantRepository.save(tenant);
        
        // 3. 初始化Schema的表結構
        initializeSchema(tenant.getSchemaName());
    }
    
    public void deleteTenant(String tenantId) {
        Tenant tenant = tenantRepository.findById(tenantId)
                .orElseThrow(() -> new RuntimeException("Tenant not found: " + tenantId));
        
        // 1. 刪除Schema
        dropSchema(tenant.getSchemaName());
        
        // 2. 刪除租戶信息
        tenantRepository.delete(tenant);
    }
    
    private void createSchemaIfNotExists(String schema) {
        try (Connection connection = dataSource.getConnection()) {
            String sql = "CREATE SCHEMA IF NOT EXISTS " + schema;
            try (Statement stmt = connection.createStatement()) {
                stmt.execute(sql);
            }
        } catch (SQLException e) {
            throw new RuntimeException("Failed to create schema: " + schema, e);
        }
    }
    
    private void dropSchema(String schema) {
        try (Connection connection = dataSource.getConnection()) {
            String sql = "DROP SCHEMA IF EXISTS " + schema + " CASCADE";
            try (Statement stmt = connection.createStatement()) {
                stmt.execute(sql);
            }
        } catch (SQLException e) {
            throw new RuntimeException("Failed to drop schema: " + schema, e);
        }
    }
    
    private void initializeSchema(String schemaName) {
        // 設置當前租戶上下文
        String previousTenant = TenantContextHolder.getTenantId();
        try {
            TenantContextHolder.setTenantId(schemaName);
            
            // 使用JPA/Hibernate工具初始化Schema
            // 可以使用SchemaExport或更推薦使用Flyway/Liquibase
            Session session = entityManagerFactory.createEntityManager().unwrap(Session.class);
            session.doWork(connection -> {
                // 執(zhí)行DDL語句
            });
            
        } finally {
            // 恢復之前的租戶上下文
            if (previousTenant != null) {
                TenantContextHolder.setTenantId(previousTenant);
            } else {
                TenantContextHolder.clear();
            }
        }
    }
}

7. 租戶管理API

@RestController
@RequestMapping("/admin/tenants")
public class TenantSchemaController {
    
    @Autowired
    private TenantSchemaManagementService tenantService;
    
    @Autowired
    private TenantRepository tenantRepository;
    
    @GetMapping
    public List<Tenant> getAllTenants() {
        return tenantRepository.findAll();
    }
    
    @PostMapping
    public ResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {
        tenantService.createTenant(tenant);
        return ResponseEntity.status(HttpStatus.CREATED).body(tenant);
    }
    
    @DeleteMapping("/{tenantId}")
    public ResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {
        tenantService.deleteTenant(tenantId);
        return ResponseEntity.noContent().build();
    }
}

優(yōu)缺點分析

優(yōu)點:

• 資源利用率高于獨立數(shù)據(jù)庫模式

• 較好的數(shù)據(jù)隔離性

• 運維復雜度低于獨立數(shù)據(jù)庫模式

• 容易實現(xiàn)租戶特定的表結構

• 數(shù)據(jù)庫級別的權限控制

缺點:

• 數(shù)據(jù)庫管理復雜度增加

• 可能存在Schema數(shù)量限制

• 跨租戶查詢仍然困難

• 無法為不同租戶使用不同的數(shù)據(jù)庫類型

• 所有租戶共享數(shù)據(jù)庫資源,可能出現(xiàn)資源爭用

適用場景

中型SaaS應用

租戶數(shù)量中等但增長較快的場景

需要較好數(shù)據(jù)隔離但成本敏感的應用

PostgreSQL或MySQL等支持Schema/數(shù)據(jù)庫隔離的數(shù)據(jù)庫環(huán)境

方案三:共享數(shù)據(jù)庫,共享Schema,獨立表模式

原理與特點

在這種模式下,所有租戶共享同一個數(shù)據(jù)庫和Schema,但每個租戶有自己的表集合,通常通過表名前綴或后綴區(qū)分不同租戶的表。

實現(xiàn)步驟

1. 實現(xiàn)多租戶命名策略

@Component
public class TenantTableNameStrategy extends PhysicalNamingStrategyStandardImpl {
    
    private static final long serialVersionUID = 1L;
    
    @Override
    public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment context) {
        String tenantId = TenantContextHolder.getTenantId();
        if (tenantId != null && !tenantId.isEmpty()) {
            String tablePrefix = tenantId + "_";
            return new Identifier(tablePrefix + name.getText(), name.isQuoted());
        }
        return super.toPhysicalTableName(name, context);
    }
}

2. 配置Hibernate命名策略

@Configuration
@EnableJpaRepositories(basePackages = "com.example.repository")
@EntityScan(basePackages = "com.example.entity")
public class JpaConfig {
    
    @Autowired
    private TenantTableNameStrategy tableNameStrategy;
    
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            EntityManagerFactoryBuilder builder,
            DataSource dataSource) {
        
        Map<String, Object> properties = new HashMap<>();
        properties.put("hibernate.physical_naming_strategy", 
                tableNameStrategy);
        
        // 其他Hibernate配置...
        
        return builder
                .dataSource(dataSource)
                .packages("com.example.entity")
                .properties(properties)
                .build();
    }
}

3. 租戶實體和倉庫

@Entity
@Table(name = "tenant_info") // 避免與租戶表前綴沖突
public class Tenant {
    
    @Id
    private String id;
    
    @Column(nullable = false)
    private String name;
    
    @Column
    private boolean active = true;
    
    // getters and setters
}

@Repository
public interface TenantRepository extends JpaRepository<Tenant, String> {
    List<Tenant> findByActive(boolean active);
}

4. 表初始化管理器

@Component
public class TenantTableManager {
    
    @Autowired
    private EntityManagerFactory entityManagerFactory;
    
    @Autowired
    private TenantRepository tenantRepository;
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public void initializeTenantTables(String tenantId) {
        String previousTenant = TenantContextHolder.getTenantId();
        try {
            TenantContextHolder.setTenantId(tenantId);
            
            // 使用JPA/Hibernate初始化表結構
            // 在生產環(huán)境中,推薦使用Flyway或Liquibase進行更精細的控制
            Session session = entityManager.unwrap(Session.class);
            session.doWork(connection -> {
                // 執(zhí)行建表語句
                // 這里可以使用Hibernate的SchemaExport,但為簡化,直接使用SQL
                
                // 示例:創(chuàng)建用戶表
                String createUserTable = "CREATE TABLE IF NOT EXISTS " + tenantId + "_users (" +
                        "id BIGINT NOT NULL AUTO_INCREMENT, " +
                        "username VARCHAR(255) NOT NULL, " +
                        "email VARCHAR(255) NOT NULL, " +
                        "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
                        "PRIMARY KEY (id)" +
                        ")";
                
                try (Statement stmt = connection.createStatement()) {
                    stmt.execute(createUserTable);
                    // 創(chuàng)建其他表...
                }
            });
            
        } finally {
            if (previousTenant != null) {
                TenantContextHolder.setTenantId(previousTenant);
            } else {
                TenantContextHolder.clear();
            }
        }
    }
    
    public void dropTenantTables(String tenantId) {
        // 獲取數(shù)據(jù)庫中所有表
        try (Connection connection = entityManager.unwrap(SessionImplementor.class).connection()) {
            DatabaseMetaData metaData = connection.getMetaData();
            String tablePrefix = tenantId + "_";
            
            try (ResultSet tables = metaData.getTables(
                    connection.getCatalog(), connection.getSchema(), tablePrefix + "%", new String[]{"TABLE"})) {
                
                List<String> tablesToDrop = new ArrayList<>();
                while (tables.next()) {
                    tablesToDrop.add(tables.getString("TABLE_NAME"));
                }
                
                // 刪除所有表
                for (String tableName : tablesToDrop) {
                    try (Statement stmt = connection.createStatement()) {
                        stmt.execute("DROP TABLE " + tableName);
                    }
                }
            }
        } catch (SQLException e) {
            throw new RuntimeException("Failed to drop tenant tables", e);
        }
    }
}

5. 租戶管理服務

@Service
public class TenantTableManagementService {
    
    @Autowired
    private TenantRepository tenantRepository;
    
    @Autowired
    private TenantTableManager tableManager;
    
    @PostConstruct
    public void initializeAllTenants() {
        for (Tenant tenant : tenantRepository.findByActive(true)) {
            tableManager.initializeTenantTables(tenant.getId());
        }
    }
    
    @Transactional
    public void createTenant(Tenant tenant) {
        // 1. 保存租戶信息
        tenantRepository.save(tenant);
        
        // 2. 初始化租戶表
        tableManager.initializeTenantTables(tenant.getId());
    }
    
    @Transactional
    public void deleteTenant(String tenantId) {
        // 1. 刪除租戶表
        tableManager.dropTenantTables(tenantId);
        
        // 2. 刪除租戶信息
        tenantRepository.deleteById(tenantId);
    }
}

6. 提供租戶管理API

@RestController
@RequestMapping("/admin/tenants")
public class TenantTableController {
    
    @Autowired
    private TenantTableManagementService tenantService;
    
    @Autowired
    private TenantRepository tenantRepository;
    
    @GetMapping
    public List<Tenant> getAllTenants() {
        return tenantRepository.findAll();
    }
    
    @PostMapping
    public ResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {
        tenantService.createTenant(tenant);
        return ResponseEntity.status(HttpStatus.CREATED).body(tenant);
    }
    
    @DeleteMapping("/{tenantId}")
    public ResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {
        tenantService.deleteTenant(tenantId);
        return ResponseEntity.noContent().build();
    }
}

優(yōu)缺點分析

優(yōu)點:

• 簡單易實現(xiàn),特別是對現(xiàn)有應用的改造

• 資源利用率高

• 跨租戶查詢相對容易實現(xiàn)

• 維護成本低

• 租戶間表結構可以不同

缺點:

• 數(shù)據(jù)隔離級別較低

• 隨著租戶數(shù)量增加,表數(shù)量會急劇增長

• 數(shù)據(jù)庫對象(如表、索引)數(shù)量可能達到數(shù)據(jù)庫限制

• 備份和恢復單個租戶數(shù)據(jù)較為復雜

• 可能需要處理表名長度限制問題

適用場景

租戶數(shù)量適中且表結構相對簡單的SaaS應用

需要為不同租戶提供不同表結構的場景

快速原型開發(fā)或MVP(最小可行產品)

從單租戶向多租戶過渡的系統(tǒng)

方案四:共享數(shù)據(jù)庫,共享Schema,共享表模式

原理與特點

這是隔離級別最低但資源效率最高的方案。所有租戶共享相同的數(shù)據(jù)庫、Schema和表,通過在每個表中添加"租戶ID"列來區(qū)分不同租戶的數(shù)據(jù)。

實現(xiàn)步驟

1. 創(chuàng)建租戶感知的實體基類

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Data
public abstract class TenantAwareEntity {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "tenant_id", nullable = false)
    private String tenantId;
    
    @CreatedDate
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    
    @PrePersist
    public void onPrePersist() {
        tenantId = TenantContextHolder.getTenantId();
    }
}

2. 租戶實體和倉庫

@Entity
@Table(name = "tenants")
public class Tenant {
    
    @Id
    private String id;
    
    @Column(nullable = false)
    private String name;
    
    @Column
    private boolean active = true;
    
    // getters and setters
}

@Repository
public interface TenantRepository extends JpaRepository<Tenant, String> {
    List<Tenant> findByActive(boolean active);
}

3. 實現(xiàn)租戶數(shù)據(jù)過濾器

@Component
public class TenantFilterInterceptor implements HandlerInterceptor {
    
    @Autowired
    private EntityManager entityManager;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String tenantId = TenantContextHolder.getTenantId();
        if (tenantId != null) {
            // 設置Hibernate過濾器
            Session session = entityManager.unwrap(Session.class);
            Filter filter = session.enableFilter("tenantFilter");
            filter.setParameter("tenantId", tenantId);
            return true;
        }
        
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        return false;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                               Object handler, Exception ex) {
        Session session = entityManager.unwrap(Session.class);
        session.disableFilter("tenantFilter");
    }
}

4. 為實體添加過濾器注解

@Entity
@Table(name = "users")
@FilterDef(name = "tenantFilter", parameters = {
    @ParamDef(name = "tenantId", type = "string")
})
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public class User extends TenantAwareEntity {
    
    @Column(name = "username", nullable = false)
    private String username;
    
    @Column(name = "email", nullable = false)
    private String email;
    
    // 其他字段和方法...
}

5. 租戶管理服務

@Service
public class SharedTableTenantService {
    
    @Autowired
    private TenantRepository tenantRepository;
    
    @Autowired
    private EntityManager entityManager;
    
    @Transactional
    public void createTenant(Tenant tenant) {
        // 直接保存租戶信息
        tenantRepository.save(tenant);
        
        // 初始化租戶默認數(shù)據(jù)
        initializeTenantData(tenant.getId());
    }
    
    @Transactional
    public void deleteTenant(String tenantId) {
        // 刪除該租戶的所有數(shù)據(jù)
        deleteAllTenantData(tenantId);
        
        // 刪除租戶記錄
        tenantRepository.deleteById(tenantId);
    }
    
    private void initializeTenantData(String tenantId) {
        String previousTenant = TenantContextHolder.getTenantId();
        try {
            TenantContextHolder.setTenantId(tenantId);
            
            // 創(chuàng)建默認用戶、角色等
            // ...
            
        } finally {
            if (previousTenant != null) {
                TenantContextHolder.setTenantId(previousTenant);
            } else {
                TenantContextHolder.clear();
            }
        }
    }
    
    private void deleteAllTenantData(String tenantId) {
        // 獲取所有帶有tenant_id列的表
        List<String> tables = getTablesWithTenantIdColumn();
        
        // 從每個表中刪除該租戶的數(shù)據(jù)
        for (String table : tables) {
            entityManager.createNativeQuery("DELETE FROM " + table + " WHERE tenant_id = :tenantId")
                    .setParameter("tenantId", tenantId)
                    .executeUpdate();
        }
    }
    
    private List<String> getTablesWithTenantIdColumn() {
        List<String> tables = new ArrayList<>();
        
        try (Connection connection = entityManager.unwrap(SessionImplementor.class).connection()) {
            DatabaseMetaData metaData = connection.getMetaData();
            
            try (ResultSet rs = metaData.getTables(
                    connection.getCatalog(), connection.getSchema(), "%", new String[]{"TABLE"})) {
                
                while (rs.next()) {
                    String tableName = rs.getString("TABLE_NAME");
                    
                    // 檢查表是否有tenant_id列
                    try (ResultSet columns = metaData.getColumns(
                            connection.getCatalog(), connection.getSchema(), tableName, "tenant_id")) {
                        
                        if (columns.next()) {
                            tables.add(tableName);
                        }
                    }
                }
            }
        } catch (SQLException e) {
            throw new RuntimeException("Failed to get tables with tenant_id column", e);
        }
        
        return tables;
    }
}

6. 租戶管理API

@RestController
@RequestMapping("/admin/tenants")
public class SharedTableTenantController {
    
    @Autowired
    private SharedTableTenantService tenantService;
    
    @Autowired
    private TenantRepository tenantRepository;
    
    @GetMapping
    public List<Tenant> getAllTenants() {
        return tenantRepository.findAll();
    }
    
    @PostMapping
    public ResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {
        tenantService.createTenant(tenant);
        return ResponseEntity.status(HttpStatus.CREATED).body(tenant);
    }
    
    @DeleteMapping("/{tenantId}")
    public ResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {
        tenantService.deleteTenant(tenantId);
        return ResponseEntity.noContent().build();
    }
}

優(yōu)缺點分析

優(yōu)點:

• 資源利用率最高

• 維護成本最低

• 實現(xiàn)簡單,對現(xiàn)有單租戶系統(tǒng)改造容易

• 跨租戶查詢簡單

• 節(jié)省存儲空間,特別是當數(shù)據(jù)量小時

缺點:

• 數(shù)據(jù)隔離級別最低

• 安全風險較高,一個錯誤可能導致跨租戶數(shù)據(jù)泄露

• 所有租戶共享相同的表結構

• 需要在所有數(shù)據(jù)訪問層強制租戶過濾

適用場景

租戶數(shù)量多但每個租戶數(shù)據(jù)量小的場景

成本敏感的應用

原型驗證或MVP階段

方案五:混合租戶模式

原理與特點

混合租戶模式結合了多種隔離策略,根據(jù)租戶等級、重要性或特定需求為不同租戶提供不同級別的隔離。例如,免費用戶可能使用共享表模式,而付費企業(yè)用戶可能使用獨立數(shù)據(jù)庫模式。

實現(xiàn)步驟

1. 租戶類型和存儲

@Entity
@Table(name = "tenants")
public class Tenant {
    
    @Id
    private String id;
    
    @Column(nullable = false)
    private String name;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private TenantType type;
    
    @Column
    private String databaseUrl;
    
    @Column
    private String username;
    
    @Column
    private String password;
    
    @Column
    private String driverClassName;
    
    @Column
    private String schemaName;
    
    @Column
    private boolean active = true;
    
    public enum TenantType {
        DEDICATED_DATABASE,
        DEDICATED_SCHEMA,
        DEDICATED_TABLE,
        SHARED_TABLE
    }
    
    // getters and setters
}

@Repository
public interface TenantRepository extends JpaRepository<Tenant, String> {
    List<Tenant> findByActive(boolean active);
    List<Tenant> findByType(Tenant.TenantType type);
}

2. 創(chuàng)建租戶分類策略

@Component
public class TenantIsolationStrategy {
    
    @Autowired
    private TenantRepository tenantRepository;
    
    private final Map<String, Tenant> tenantCache = new ConcurrentHashMap<>();
    
    @PostConstruct
    public void loadTenants() {
        tenantRepository.findByActive(true).forEach(tenant -> 
            tenantCache.put(tenant.getId(), tenant));
    }
    
    public Tenant.TenantType getIsolationTypeForTenant(String tenantId) {
        Tenant tenant = tenantCache.get(tenantId);
        if (tenant == null) {
            tenant = tenantRepository.findById(tenantId)
                    .orElseThrow(() -> new RuntimeException("Tenant not found: " + tenantId));
            tenantCache.put(tenantId, tenant);
        }
        return tenant.getType();
    }
    
    public Tenant getTenant(String tenantId) {
        Tenant tenant = tenantCache.get(tenantId);
        if (tenant == null) {
            tenant = tenantRepository.findById(tenantId)
                    .orElseThrow(() -> new RuntimeException("Tenant not found: " + tenantId));
            tenantCache.put(tenantId, tenant);
        }
        return tenant;
    }
    
    public void evictFromCache(String tenantId) {
        tenantCache.remove(tenantId);
    }
}

3. 實現(xiàn)混合數(shù)據(jù)源路由

@Component
public class HybridTenantRouter {
    
    @Autowired
    private TenantIsolationStrategy isolationStrategy;
    
    private final Map<String, DataSource> dedicatedDataSources = new ConcurrentHashMap<>();
    
    @Autowired
    private DataSource sharedDataSource;
    
    public DataSource getDataSourceForTenant(String tenantId) {
        Tenant.TenantType isolationType = isolationStrategy.getIsolationTypeForTenant(tenantId);
        
        if (isolationType == Tenant.TenantType.DEDICATED_DATABASE) {
            // 對于獨立數(shù)據(jù)庫的租戶,查找或創(chuàng)建專用數(shù)據(jù)源
            return dedicatedDataSources.computeIfAbsent(tenantId, this::createDedicatedDataSource);
        }
        
        return sharedDataSource;
    }
    
    private DataSource createDedicatedDataSource(String tenantId) {
        Tenant tenant = isolationStrategy.getTenant(tenantId);
        
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(tenant.getDatabaseUrl());
        dataSource.setUsername(tenant.getUsername());
        dataSource.setPassword(tenant.getPassword());
        dataSource.setDriverClassName(tenant.getDriverClassName());
        
        return dataSource;
    }
    
    public void removeDedicatedDataSource(String tenantId) {
        DataSource dataSource = dedicatedDataSources.remove(tenantId);
        if (dataSource instanceof HikariDataSource) {
            ((HikariDataSource) dataSource).close();
        }
    }
}

4. 混合租戶路由數(shù)據(jù)源

public class HybridRoutingDataSource extends AbstractRoutingDataSource {
    
    @Autowired
    private HybridTenantRouter tenantRouter;
    
    @Autowired
    private TenantIsolationStrategy isolationStrategy;
    
    @Override
    protected Object determineCurrentLookupKey() {
        String tenantId = TenantContextHolder.getTenantId();
        if (tenantId == null) {
            return "default";
        }
        
        Tenant.TenantType isolationType = isolationStrategy.getIsolationTypeForTenant(tenantId);
        
        if (isolationType == Tenant.TenantType.DEDICATED_DATABASE) {
            return tenantId;
        }
        
        return "shared";
    }
    
    @Override
    protected DataSource determineTargetDataSource() {
        String tenantId = TenantContextHolder.getTenantId();
        if (tenantId == null) {
            return super.determineTargetDataSource();
        }
        
        return tenantRouter.getDataSourceForTenant(tenantId);
    }
}

5. 混合租戶攔截器

@Component
public class HybridTenantInterceptor implements HandlerInterceptor {
    
    @Autowired
    private TenantIsolationStrategy isolationStrategy;
    
    @Autowired
    private EntityManager entityManager;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String tenantId = extractTenantId(request);
        if (tenantId != null) {
            TenantContextHolder.setTenantId(tenantId);
            
            Tenant.TenantType isolationType = isolationStrategy.getIsolationTypeForTenant(tenantId);
            
            // 根據(jù)隔離類型應用不同策略
            switch (isolationType) {
                case DEDICATED_DATABASE:
                    // 已由數(shù)據(jù)源路由處理
                    break;
                case DEDICATED_SCHEMA:
                    setSchema(isolationStrategy.getTenant(tenantId).getSchemaName());
                    break;
                case DEDICATED_TABLE:
                    // 由命名策略處理
                    break;
                case SHARED_TABLE:
                    enableTenantFilter(tenantId);
                    break;
            }
            
            return true;
        }
        
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        return false;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                               Object handler, Exception ex) {
        String tenantId = TenantContextHolder.getTenantId();
        if (tenantId != null) {
            Tenant.TenantType isolationType = isolationStrategy.getIsolationTypeForTenant(tenantId);
            
            if (isolationType == Tenant.TenantType.SHARED_TABLE) {
                disableTenantFilter();
            }
        }
        
        TenantContextHolder.clear();
    }
    
    private void setSchema(String schema) {
        try {
            entityManager.createNativeQuery("SET SCHEMA '" + schema + "'").executeUpdate();
        } catch (Exception e) {
            // 處理異常
        }
    }
    
    private void enableTenantFilter(String tenantId) {
        Session session = entityManager.unwrap(Session.class);
        Filter filter = session.enableFilter("tenantFilter");
        filter.setParameter("tenantId", tenantId);
    }
    
    private void disableTenantFilter() {
        Session session = entityManager.unwrap(Session.class);
        session.disableFilter("tenantFilter");
    }
    
    private String extractTenantId(HttpServletRequest request) {
        // 從請求中提取租戶ID的邏輯
        return request.getHeader("X-TenantID");
    }
}

6. 綜合租戶管理服務

@Service
public class HybridTenantManagementService {
    
    @Autowired
    private TenantRepository tenantRepository;
    
    @Autowired
    private TenantIsolationStrategy isolationStrategy;
    
    @Autowired
    private HybridTenantRouter tenantRouter;
    
    @Autowired
    private EntityManager entityManager;
    
    @Autowired
    private DataSource dataSource;
    
    // 不同隔離類型的初始化策略
    private final Map<Tenant.TenantType, TenantInitializer> initializers = new HashMap<>();
    
    @PostConstruct
    public void init() {
        initializers.put(Tenant.TenantType.DEDICATED_DATABASE, this::initializeDedicatedDatabase);
        initializers.put(Tenant.TenantType.DEDICATED_SCHEMA, this::initializeDedicatedSchema);
        initializers.put(Tenant.TenantType.DEDICATED_TABLE, this::initializeDedicatedTables);
        initializers.put(Tenant.TenantType.SHARED_TABLE, this::initializeSharedTables);
    }
    
    @Transactional
    public void createTenant(Tenant tenant) {
        // 1. 保存租戶基本信息
        tenantRepository.save(tenant);
        
        // 2. 根據(jù)隔離類型初始化
        TenantInitializer initializer = initializers.get(tenant.getType());
        if (initializer != null) {
            initializer.initialize(tenant);
        }
        
        // 3. 更新緩存
        isolationStrategy.evictFromCache(tenant.getId());
    }
    
    @Transactional
    public void deleteTenant(String tenantId) {
        Tenant tenant = tenantRepository.findById(tenantId)
                .orElseThrow(() -> new RuntimeException("Tenant not found: " + tenantId));
        
        // 1. 根據(jù)隔離類型清理資源
        switch (tenant.getType()) {
            case DEDICATED_DATABASE:
                cleanupDedicatedDatabase(tenant);
                break;
            case DEDICATED_SCHEMA:
                cleanupDedicatedSchema(tenant);
                break;
            case DEDICATED_TABLE:
                cleanupDedicatedTables(tenant);
                break;
            case SHARED_TABLE:
                cleanupSharedTables(tenant);
                break;
        }
        
        // 2. 刪除租戶信息
        tenantRepository.delete(tenant);
        
        // 3. 更新緩存
        isolationStrategy.evictFromCache(tenantId);
    }
    
    // 獨立數(shù)據(jù)庫初始化
    private void initializeDedicatedDatabase(Tenant tenant) {
        // 創(chuàng)建數(shù)據(jù)源
        DataSource dedicatedDs = tenantRouter.getDataSourceForTenant(tenant.getId());
        
        // 初始化數(shù)據(jù)庫結構
        try (Connection conn = dedicatedDs.getConnection()) {
            // 執(zhí)行DDL腳本
            // ...
        } catch (SQLException e) {
            throw new RuntimeException("Failed to initialize database for tenant: " + tenant.getId(), e);
        }
    }
    
    // Schema初始化
    private void initializeDedicatedSchema(Tenant tenant) {
        try (Connection conn = dataSource.getConnection()) {
            // 創(chuàng)建Schema
            try (Statement stmt = conn.createStatement()) {
                stmt.execute("CREATE SCHEMA IF NOT EXISTS " + tenant.getSchemaName());
            }
            
            // 切換到該Schema
            conn.setSchema(tenant.getSchemaName());
            
            // 創(chuàng)建表結構
            // ...
            
        } catch (SQLException e) {
            throw new RuntimeException("Failed to initialize schema for tenant: " + tenant.getId(), e);
        }
    }
    
    // 獨立表初始化
    private void initializeDedicatedTables(Tenant tenant) {
        // 設置線程上下文中的租戶ID以使用正確的表名前綴
        String previousTenant = TenantContextHolder.getTenantId();
        try {
            TenantContextHolder.setTenantId(tenant.getId());
            
            // 創(chuàng)建表
            // ...
            
        } finally {
            if (previousTenant != null) {
                TenantContextHolder.setTenantId(previousTenant);
            } else {
                TenantContextHolder.clear();
            }
        }
    }
    
    // 共享表初始化
    private void initializeSharedTables(Tenant tenant) {
        // 共享表模式下,只需插入租戶特定的初始數(shù)據(jù)
        String previousTenant = TenantContextHolder.getTenantId();
        try {
            TenantContextHolder.setTenantId(tenant.getId());
            
            // 插入初始數(shù)據(jù)
            // ...
            
        } finally {
            if (previousTenant != null) {
                TenantContextHolder.setTenantId(previousTenant);
            } else {
                TenantContextHolder.clear();
            }
        }
    }
    
    // 清理方法
    private void cleanupDedicatedDatabase(Tenant tenant) {
        // 關閉并移除數(shù)據(jù)源
        tenantRouter.removeDedicatedDataSource(tenant.getId());
        
        // 注意:通常不會自動刪除實際的數(shù)據(jù)庫,這需要DBA手動操作
    }
    
    private void cleanupDedicatedSchema(Tenant tenant) {
        try (Connection conn = dataSource.getConnection()) {
            try (Statement stmt = conn.createStatement()) {
                stmt.execute("DROP SCHEMA IF EXISTS " + tenant.getSchemaName() + " CASCADE");
            }
        } catch (SQLException e) {
            throw new RuntimeException("Failed to drop schema for tenant: " + tenant.getId(), e);
        }
    }
    
    private void cleanupDedicatedTables(Tenant tenant) {
        // 查找并刪除該租戶的所有表
        try (Connection conn = dataSource.getConnection()) {
            DatabaseMetaData metaData = conn.getMetaData();
            String tablePrefix = tenant.getId() + "_";
            
            try (ResultSet tables = metaData.getTables(
                    conn.getCatalog(), conn.getSchema(), tablePrefix + "%", new String[]{"TABLE"})) {
                
                while (tables.next()) {
                    String tableName = tables.getString("TABLE_NAME");
                    try (Statement stmt = conn.createStatement()) {
                        stmt.execute("DROP TABLE " + tableName);
                    }
                }
            }
        } catch (SQLException e) {
            throw new RuntimeException("Failed to drop tables for tenant: " + tenant.getId(), e);
        }
    }
    
    private void cleanupSharedTables(Tenant tenant) {
        // 從所有帶有tenant_id列的表中刪除該租戶的數(shù)據(jù)
        entityManager.createNativeQuery(
                "SELECT table_name FROM information_schema.columns " +
                "WHERE column_name = 'tenant_id'")
                .getResultList()
                .forEach(tableName -> 
                    entityManager.createNativeQuery(
                            "DELETE FROM " + tableName + " WHERE tenant_id = :tenantId")
                            .setParameter("tenantId", tenant.getId())
                            .executeUpdate()
                );
    }
    
    // 租戶初始化策略接口
    @FunctionalInterface
    private interface TenantInitializer {
        void initialize(Tenant tenant);
    }
}

7. 提供租戶管理API

@RestController
@RequestMapping("/admin/tenants")
public class HybridTenantController {
    
    @Autowired
    private HybridTenantManagementService tenantService;
    
    @Autowired
    private TenantRepository tenantRepository;
    
    @GetMapping
    public List<Tenant> getAllTenants() {
        return tenantRepository.findAll();
    }
    
    @PostMapping
    public ResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {
        tenantService.createTenant(tenant);
        return ResponseEntity.status(HttpStatus.CREATED).body(tenant);
    }
    
    @PutMapping("/{tenantId}")
    public ResponseEntity<Tenant> updateTenant(
            @PathVariable String tenantId, 
            @RequestBody Tenant tenant) {
        
        tenant.setId(tenantId);
        tenantService.updateTenant(tenant);
        return ResponseEntity.ok(tenant);
    }
    
    @DeleteMapping("/{tenantId}")
    public ResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {
        tenantService.deleteTenant(tenantId);
        return ResponseEntity.noContent().build();
    }
    
    @GetMapping("/types")
    public ResponseEntity<List<Tenant.TenantType>> getTenantTypes() {
        return ResponseEntity.ok(Arrays.asList(Tenant.TenantType.values()));
    }
}

優(yōu)缺點分析

優(yōu)點:

• 最大的靈活性,可根據(jù)租戶需求提供不同隔離級別

• 可以實現(xiàn)資源和成本的平衡

• 可以根據(jù)業(yè)務價值分配資源

• 適應不同客戶的安全和性能需求

缺點:

• 實現(xiàn)復雜度最高

• 維護和測試成本高

• 需要處理多種數(shù)據(jù)訪問模式

• 可能引入不一致的用戶體驗

  • • 錯誤處理更加復雜

適用場景

需要提供靈活定價模型的應用

資源需求差異大的租戶集合

方案對比

隔離模式數(shù)據(jù)隔離級別資源利用率成本復雜度適用場景
獨立數(shù)據(jù)庫最高企業(yè)級應用、金融/醫(yī)療行業(yè)
獨立Schema中型SaaS、安全要求較高的場景
獨立表中高中低中小型應用、原型驗證
共享表最高大量小租戶、成本敏感場景
混合模式可變可變中高多層級服務、復雜業(yè)務需求

總結

多租戶架構是構建現(xiàn)代SaaS應用的關鍵技術,選擇多租戶模式需要平衡數(shù)據(jù)隔離、資源利用、成本和復雜度等多種因素。

通過深入理解這些架構模式及其權衡,可以根據(jù)實際情況選擇適合的多租戶架構,構建可擴展、安全且經(jīng)濟高效的企業(yè)級應用。

以上就是SpringBoot實現(xiàn)多租戶系統(tǒng)架構的5種設計方案介紹的詳細內容,更多關于SpringBoot多租戶架構的資料請關注腳本之家其它相關文章!

相關文章

  • Java for循環(huán)的幾種用法分析

    Java for循環(huán)的幾種用法分析

    本篇文章小編為大家介紹,Java for循環(huán)的幾種用法分析。需要的朋友參考下
    2013-04-04
  • 詳解JavaWeb中的 Listener

    詳解JavaWeb中的 Listener

    JavaWeb里面的listener是通過觀察者設計模式進行實現(xiàn)的。下面通過本文給大家詳細介紹javaweb中的listener,感興趣的朋友一起看看吧
    2016-09-09
  • elasticsearch構造Client實現(xiàn)java客戶端調用接口示例分析

    elasticsearch構造Client實現(xiàn)java客戶端調用接口示例分析

    這篇文章主要為大家介紹了elasticsearch構造Client實現(xiàn)java客戶端調用接口示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-04-04
  • Springboot整合minio實現(xiàn)文件服務的教程詳解

    Springboot整合minio實現(xiàn)文件服務的教程詳解

    這篇文章主要介紹了Springboot整合minio實現(xiàn)文件服務的教程,文中的示例代碼講解詳細,對我們的工作或學習有一定幫助,需要的可以參考一下
    2022-06-06
  • form表單回寫技術java實現(xiàn)

    form表單回寫技術java實現(xiàn)

    這篇文章主要為大家詳細介紹了java實現(xiàn)form表單回寫技術的相關資料,需要的朋友可以參考下
    2016-04-04
  • 詳解Java實踐之抽象工廠模式

    詳解Java實踐之抽象工廠模式

    抽象工廠模式用于產品族的構建。抽象工廠是所有形態(tài)的工廠模式中最為抽象和最具一般性的一種形態(tài)。抽象工廠是指當有多個抽象角色時使用的一種工廠模式。抽象工廠模式可以向客戶端提供一個接口,使客戶端在不必指定產品的具體情況下,創(chuàng)建多個產品族中的產品對象
    2021-06-06
  • JDBC連接數(shù)據(jù)庫步驟及基本操作示例詳解

    JDBC連接數(shù)據(jù)庫步驟及基本操作示例詳解

    這篇文章主要為大家介紹了JDBC連接數(shù)據(jù)庫步驟及基本操作示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-11-11
  • SpringBoot+WebSocket實現(xiàn)即時通訊功能(Spring方式)

    SpringBoot+WebSocket實現(xiàn)即時通訊功能(Spring方式)

    今天給大家分享一個SpringBoot+WebSocket實現(xiàn)即時通訊功能(Spring方式),WebSocket是一種在單個TCP連接上進行全雙工通信的協(xié)議,文章通過代碼示例給大家介紹的非常詳細,需要的朋友可以參考下
    2023-10-10
  • Spring Boot構建優(yōu)雅的RESTful接口過程詳解

    Spring Boot構建優(yōu)雅的RESTful接口過程詳解

    這篇文章主要介紹了spring boot構建優(yōu)雅的RESTful接口過程詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2019-08-08
  • Spring?security?oauth2以redis作為tokenstore及jackson序列化失敗問題

    Spring?security?oauth2以redis作為tokenstore及jackson序列化失敗問題

    這篇文章主要介紹了Spring?security?oauth2以redis作為tokenstore及jackson序列化失敗問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教<BR>
    2024-04-04

最新評論