Spring BootでマルチテナントSaaSプラットフォームを構築するコア技術についてお話しします

Spring BootでマルチテナントSaaSプラットフォームを構築するコア技術についてお話しします

[[326138]]

1. 概要

筆者は、2014年よりマルチテナント(またはマルチテナント)のソフトウェアアプリケーションプラットフォームであるSaaS(Software as a Service)に携わり、関連分野のアーキテクチャ設計や研究開発業務に従事してきました。偶然にも、学部の卒業プロジェクト中に、SaaS ベースの効率的な財務管理プラットフォームに関する研究プロジェクトを完了し、そこから多くのことを得ることができました。私が初めてSaaSに触れた頃、中国には関連リソースが不足しており、参考資料は「インターネット時代のソフトウェア革命:SaaSアーキテクチャ設計」(Ye Wei他著)という書籍のみでした。最終的なプロジェクトは、OSGI (Open Service Gateway Initiative) Java 動的モジュラー システム仕様に基づいて実装されます。

それから5年が経ち、ソフトウェア開発技術は大きな変化を遂げました。著者が実装した SaaS プラットフォームのテクノロジー スタックも数回更新されており、「出口がないように見えるときでも、振り返れば出口が見つかる」という格言をまさに証明しています。これまでに遭遇した多くの回り道や落とし穴、そして最近多くのネットユーザーから Spring Boot を使用してマルチテナント システムを実装する方法を尋ねられたという事実に基づいて、SaaS のハードコア技術について語る記事を書くことにしました。

SaaS といえば、それは単なるソフトウェア アーキテクチャです。それほど謎めいたところはなく、非常に難しいシステムでもありません。私個人としては、SaaS プラットフォームの難しさは技術的な実装ではなく、商業的な運用にあると感じています。技術的に言えば、SaaS は、複数の異なる環境のユーザーが同じアプリケーション セットを使用できるようにしながら、ユーザー間のデータが互いに分離されていることを保証するアーキテクチャ モデルです。考えてみれば、これもシェアリングエコノミーの雰囲気が少しありますね。

ここでは、SaaS ソフトウェア成熟モデルとデータ分離ソリューションの比較については詳しく説明しません。本日は、Spring Boot を使用して、独立データベース/共有データベース独立スキーマを備えたマルチテナント システムを迅速に構築する方法について説明します。私は SaaS システムのコア技術実装を提供し、興味のある友人はこれを基に拡張することができます。

2. マルチテナントのアプリケーションシナリオを理解する

アプリケーションを開発する必要があり、同じアプリケーションを N 人の顧客に販売したいとします。通常の状況では、N 台の Web サーバー (Tomcat)、N 台のデータベース (DB) を作成し、N 人の顧客に対して同じアプリケーションを N 回展開する必要があります。ここで、アプリケーションがアップグレードされたり、その他の変更が行われたりすると、N 個のアプリケーションを更新し、N 個のサーバーを保守する必要があります。次に、ビジネスが成長し、顧客数が当初の N から現在の N+M に増加すると、N 個のアプリケーションと M 個のアプリケーション バージョンの維持、機器のメンテナンス、コスト管理という問題に直面することになります。運用保守スタッフはコンピューター室で泣きそうになりました...

上記の問題を解決するには、マルチテナント アプリケーションを開発し、現在のユーザーに基づいて対応するデータベースを選択します。たとえば、A 社のユーザーからリクエストが来ると、アプリケーションは A 社のデータベースに接続します。 B 社のユーザーからリクエストが来ると、アプリケーションは自動的にデータベースを B 社のデータベースに切り替えます。理論的には問題はありませんが、既存のアプリケーションを SaaS モードに変換することを検討すると、最初の問題に遭遇します。それは、リクエストがどのテナントから送信されたかをどのように識別するかということです。データソースを自動的に切り替えるにはどうすればいいですか?

3. テナントデータソースの維持、識別、ルーティング

データベース名、リンクアドレス、ユーザー名、パスワードなどのテナント情報を保存するための独立したライブラリを提供できるため、テナント情報のメンテナンスの問題を統一的に解決できます。テナントの識別とルーティングを解決する方法は多数あります。一般的な方法をいくつか紹介します。

  • 1. テナントはドメイン名で識別できます。各テナントに一意の第 2 レベル ドメイン名を提供し、それを通じてテナントを識別できます (例: tenantone.example.com、tenant.example.com)。 tenantone と tenant は、テナントを識別するための重要な情報です。
  • 2. テナント情報は、saas.example.com?tenantId=tenant1、saas.example.com?tenantId=tenant2 のように、サーバーがテナントを識別できるようにサポートするために、リクエスト パラメーターとしてサーバーに渡すことができます。パラメーター tenantId は、アプリケーションがテナントを識別するための重要な情報です。
  • 3. テナント情報は、JWT などのテクノロジーを使用してリクエスト ヘッダーに設定できます。サーバーは、ヘッダー内の関連パラメータを解析してテナント情報を取得します。
  • 4. ユーザーがシステムに正常にログインすると、テナント情報はセッションに保存され、必要に応じてセッションから取得されます。

上記の問題を解決した後、クライアントから渡されたテナント情報を取得する方法と、ビジネス コードでテナント情報を使用する方法を見てみましょう (最も重要な問題は DataSources の問題です)。

Spring Boot アプリケーションを起動する前に、データ ソース (データベースが使用されている場合) に関する構成情報を提供する必要があることは誰もが知っています。初期要件によると、アプリケーションを使用する必要がある顧客は N 人いるので、事前に N 個のデータ ソース (複数のデータ ソース) を構成する必要があります。 N<50であれば、まだ許容できると思います。それ以上であれば、明らかに受け入れられません。この問題を解決するには、Hibernate 5 が提供する動的データ ソース機能を使用して、アプリケーションがクライアント データ ソースを動的に構成できるようにする必要があります。簡単に言うと、ユーザーがシステム リソースを要求すると、ユーザーが提供するテナント情報 (tenantId) を ThreadLoacal に保存し、TheadLocal でテナント情報を取得し、この情報に基づいて別のテナント ライブラリを照会して、現在のテナントのデータ構成情報を取得します。次に、データ ソースを動的に構成する Hibernate の機能を利用して、現在のリクエストのデータ ソースを設定し、最後に前のユーザーのリクエストのデータ ソースを設定します。この方法では、アプリケーション内でデータ ソース構成情報 (テナント データベース構成ライブラリ) のコピーを 1 つだけ保持すればよく、残りのデータ ソースは動的にクエリおよび構成されます。次に、この機能を簡単に説明します。

4. プロジェクトの構築

このデモ プロジェクトを実装するには、Spring Boot 2.1.5 を使用します。まず、Maven 構成ファイルに次の構成を追加する必要があります。

  1. <依存関係>
  2. <依存関係>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>スプリングブートスターター</artifactId>
  5. </依存関係>
  6.  
  7. <依存関係>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-devtools</artifactId>
  10. <scope>ランタイム</scope>
  11. </依存関係>
  12. <依存関係>
  13. <groupId>org.projectlombok</groupId>
  14. <artifactId>ロンボク</artifactId>
  15. <オプション> true </オプション>
  16. </依存関係>
  17. <依存関係>
  18. <groupId>org.springframework.boot</groupId>
  19. <artifactId>spring-boot-starter-test</artifactId>
  20. <scope>テスト</scope>
  21. </依存関係>
  22. <依存関係>
  23. <groupId>org.springframework.boot</groupId>
  24. <artifactId>spring-boot-starter-data-jpa</artifactId>
  25. </依存関係>
  26. <依存関係>
  27. <groupId>org.springframework.boot</groupId>
  28. <artifactId>spring-boot-starter-web</artifactId>
  29. </依存関係>
  30. <依存関係>
  31. <groupId>org.springframework.boot</groupId>
  32. <artifactId>spring-boot-configuration-processor</artifactId>
  33. </依存関係>
  34. <依存関係>
  35. <グループID>mysql</グループID>
  36. <artifactId>mysql-コネクタ-java</artifactId>
  37. <バージョン>5.1.47</バージョン>
  38. </依存関係>
  39. <依存関係>
  40. <groupId>org.springframework.boot</groupId>
  41. <artifactId>spring-boot-starter-freemarker</artifactId>
  42. </依存関係>
  43. <依存関係>
  44. <グループ ID>org.apache.commons</グループ ID>
  45. <artifactId>コモンズ-lang3</artifactId>
  46. </依存関係>
  47. </依存関係>

次に、利用可能な構成ファイルを提供し、次のコンテンツを追加します。

  1. 春:
  2. フリーマーカー:
  3. キャッシュ: false  
  4. テンプレートローダーパス:
  5. - クラスパス:/テンプレート/
  6. プレフィックス:
  7. サフィックス: .html
  8. リソース:
  9. 静的場所:
  10. - クラスパス:/ static /
  11. 開発ツール:
  12. 再起動:
  13. 有効: true  
  14. jp: 日本語:
  15. データベース: MySQL
  16. 表示SQL: true  
  17. 生成DDL: false  
  18. 休止状態:
  19. ddl-auto: なし
  20. ウナ:
  21. マスター:
  22. データソース:
  23. URL: jdbc:mysql://localhost:3306/master_tenant?useSSL= false  
  24. ユーザー名: root
  25. パスワード: root
  26. ドライバークラス名: com.mysql.jdbc.Driver
  27. 最大プールサイズ: 10
  28. アイドルタイムアウト: 300000
  29. 最小アイドル: 10
  30. プール名: マスター -データベース-接続- プール
  31. ログ記録:
  32. レベル
  33. ルート:警告
  34. 組織:
  35. スプリングフレームワーク:
  36. ウェブ:デバッグ
  37. 休止状態:デバッグ

Freemarkerはビューレンダリングエンジンとして使用されるため、Freemarker関連の技術を提供する必要がある。

una:master:datasource 設定項目は、テナント情報を一元的に格納する前述のデータ ソース設定情報です。これをマスター データベースと考えることができます。

次に、Spring Boot の自動データ ソース構成機能をオフにし、プロジェクトのメイン クラスに次の設定を追加する必要があります。

  1. @SpringBootApplication({DataSourceAutoConfiguration.class} を除外)
  2. パブリッククラス UnaSaasApplication {
  3.  
  4. 公共 静的void main(String[] args) {
  5. SpringApplication.run(UnaSaasApplication.class、引数);
  6. }
  7.  
  8. }

最後に、プロジェクト全体の構造を見てみましょう。

5. テナントデータソースクエリモジュールを実装する

テナント名、データベース接続アドレス、ユーザー名、パスワードなどを含むテナント データ ソース情報を格納するエンティティ クラスを定義します。コードは次のとおりです。

  1. @データ
  2. @実在物
  3. @テーブル(名前= "MASTER_TENANT" )
  4. @NoArgsコンストラクタ
  5. @AllArgsコンストラクタ
  6. @ビルダー
  7. パブリッククラスMasterTenantはSerializableを実装します{
  8.  
  9. @ID
  10. @(名前= "ID" )
  11. プライベート文字列ID;
  12.  
  13. @(名前= "TENANT" )
  14. @NotEmpty(message = "テナント識別子を指定する必要があります" )
  15. プライベート文字列テナント;
  16.  
  17. @(名前= "URL" )
  18. @サイズ(最大= 256)
  19. @NotEmpty(message = "テナントのJDBC URLを指定する必要があります" )
  20. プライベート文字列 URL;
  21.  
  22. @(名前= "ユーザー名" )
  23. @サイズ(最小= 4、最大= 30、メッセージ = "db ユーザー名の長さは 4 から 30 の間でなければなりません" )
  24. @NotEmpty(message = "テナント DB ユーザー名を指定する必要があります" )
  25. プライベート文字列ユーザー名;
  26.  
  27. @(名前= "パスワード" )
  28. @サイズ(最小= 4、最大= 30 )
  29. @NotEmpty(message = "テナント DB パスワードを入力する必要があります" )
  30. プライベート文字列パスワード;
  31.  
  32. @バージョン
  33. プライベートintバージョン = 0;
  34. }

永続化レイヤーでは、JpaRepository インターフェースを継承して、データ ソースに対して CURD 操作を迅速に実装します。同時に、テナント名でテナント データ ソースを検索するためのインターフェイスも提供します。コードは次のとおりです。

  1. パッケージ com.ramostear.una.saas.master.repository;
  2.  
  3. com.ramostear.una.saas.master.model.MasterTenant をインポートします。
  4. org.springframework.data.jpa.repository.JpaRepository をインポートします。
  5. org.springframework.data.jpa.repository.Query をインポートします。
  6. org.springframework.data.repository.query.Param をインポートします。
  7. org.springframework.stereotype.Repository をインポートします。
  8.  
  9. /**
  10. * @author : Tan Chaohong (別名:ramostear)によって作成されました
  11. * @作成日時2019/5/25 0025-8:22
  12. * @修正する による
  13. * @以来:
  14. */
  15. @リポジトリ
  16. パブリックインターフェースMasterTenantRepositoryはJpaRepository<MasterTenant,String>を拡張します{
  17.  
  18. @Query( "MasterTenant p から p を選択し、p.tenant = :tenant の場合" )
  19. マスターテナント findByTenant(@Param( "tenant" ) String テナント);
  20. }

ビジネス レイヤーは、テナント名でテナント データ ソース情報を取得するためのサービスを提供します (他のサービスはユーザーが追加できます)。

  1. パッケージ com.ramostear.una.saas.master.service;
  2.  
  3. com.ramostear.una.saas.master.model.MasterTenant をインポートします。
  4.  
  5. /**
  6. * @author : Tan Chaohong (別名:ramostear)によって作成されました
  7. * @作成日時2019/5/25 0025-8:26
  8. * @修正する による
  9. * @以来:
  10. */
  11.  
  12. パブリックインターフェースMasterTenantService {
  13. /**
  14. * カスタムテナントクエリの使用
  15. * @param テナント テナント 
  16. * @マスターテナントを返す
  17. */
  18. マスターテナント findByTenant(String テナント);
  19. }

最後に、重点を置く必要がある主なポイントは、プライマリ データ ソースを構成することです (Spring Boot は、そのためのデフォルトのデータ ソースを提供する必要があります)。設定する前に、設定項目を取得する必要があります。 @ConfigurationProperties("una.master.datasource") を通じて、構成ファイル内の関連する構成情報を取得できます。

  1. @ゲッター
  2. @セッター
  3. @構成
  4. @ConfigurationProperties( "una.master.datasource" )
  5. パブリッククラスMasterDatabaseProperties {
  6.  
  7. プライベート文字列 URL;
  8.  
  9. プライベート文字列パスワード;
  10.  
  11. プライベート文字列ユーザー名;
  12.  
  13. プライベート文字列driverClassName;
  14.  
  15. プライベート長い接続タイムアウト;
  16.  
  17. プライベートint maxPoolSize;
  18.  
  19. プライベート長いidleTimeout;
  20.  
  21. プライベートint minIdle;
  22.  
  23. プライベート文字列 poolName;
  24.  
  25. @オーバーライド
  26. パブリック文字列toString(){
  27. StringBuilder ビルダー = new StringBuilder();
  28. ビルダー.append( "マスターデータベースプロパティ [ url=" )
  29. .append(URL) を追加します。
  30. .append( ", ユーザー名=" )
  31. .append(ユーザー名)
  32. .append( ", パスワード=" )
  33. .append(パスワード)
  34. .append( ", ドライバークラス名=" )
  35. .append(ドライバークラス名)
  36. .append( ", 接続タイムアウト=" )
  37. .append(接続タイムアウト)
  38. .append( ", maxPoolSize=" )
  39. .append(最大プールサイズ)
  40. .append( ", アイドルタイムアウト=" )
  41. .append(アイドルタイムアウト)
  42. .append( ", minIdle=" )
  43. .append(最小アイドル)
  44. .append( ", プール名=" )
  45. .append(プール名)
  46. .append( "]" );
  47. builder.toString()を返します
  48. }
  49. }

次のステップは、カスタム データ ソースを構成することです。ソースコードは次のとおりです。

  1. パッケージ com.ramostear.una.saas.master.config;
  2.  
  3. com.ramostear.una.saas.master.config.properties.MasterDatabaseProperties をインポートします。
  4. com.ramostear.una.saas.master.model.MasterTenant をインポートします。
  5. com.ramostear.una.saas.master.repository.MasterTenantRepository をインポートします。
  6. com.zaxxer.hikari.HikariDataSource をインポートします。
  7. lombok.extern.slf4j.Slf4j をインポートします。
  8. org.hibernate.cfg.Environment をインポートします。
  9. org.springframework.beans.factory.annotation.Autowired をインポートします。
  10. org.springframework.beans.factory.annotation.Qualifier をインポートします。
  11. org.springframework.context.annotation.Bean をインポートします。
  12. org.springframework.context.annotation.Configuration をインポートします。
  13. org.springframework.context.annotation.Primaryをインポートします
  14. org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor をインポートします。
  15. org.springframework.data.jpa.repository.config.EnableJpaRepositories をインポートします。
  16. org.springframework.orm.jpa.JpaTransactionManager をインポートします。
  17. org.springframework.orm.jpa.JpaVendorAdapter をインポートします。
  18. org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean をインポートします。
  19. org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter をインポートします。
  20. org.springframework.transaction.annotation.EnableTransactionManagementをインポートします
  21.  
  22. javax.persistence.EntityManagerFactory をインポートします。
  23. javax.sql.DataSource をインポートします。
  24. java.util.Properties をインポートします。
  25.  
  26. /**
  27. * @author : Tan Chaohong (別名:ramostear)によって作成されました
  28. * @作成日時2019/5/25 0025-8:31
  29. * @修正する による
  30. * @以来:
  31. */
  32. @構成
  33. @トランザクション管理を有効にする
  34. @EnableJpaRepositories(basePackages = { "com.ramostear.una.saas.master.model" , "com.ramostear.una.saas.master.repository" },
  35. entityManagerFactoryRef = "マスターEntityManagerFactory"
  36. トランザクションマネージャ参照 = "マスタートランザクションマネージャ" )
  37. 翻訳者
  38. パブリッククラスMasterDatabaseConfig {
  39.  
  40. オートワイヤード
  41. プライベート MasterDatabaseProperties masterDatabaseProperties;
  42.  
  43. @Bean(名前= "マスターデータソース" )
  44. パブリックデータソースマスターデータソース(){
  45. log.info( "masterDatasource を次の値で設定しています:{}" ,masterDatabaseProperties.toString());
  46. HikariDataSource データソース = new HikariDataSource();
  47. データソース.setUsername(masterDatabaseProperties.getUsername());
  48. データソース.setPassword(masterDatabaseProperties.getPassword());
  49. データソース.setJdbcUrl(masterDatabaseProperties.getUrl());
  50. データソース.setDriverClassName(masterDatabaseProperties.getDriverClassName());
  51. データソース。setPoolName(masterDatabaseProperties.getPoolName());
  52. データソースの最大プールサイズを設定します(masterDatabaseProperties.getMaxPoolSize());
  53. データソースの最小アイドル値を設定します。
  54. データソース.setConnectionTimeout(masterDatabaseProperties.getConnectionTimeout());
  55. データソース.setIdleTimeout(masterDatabaseProperties.getIdleTimeout());
  56. log.info( "masterDatasource のセットアップに成功しました。" );
  57. データソースを返します
  58. }
  59. @主要な 
  60. @Bean(名前= "masterEntityManagerFactory" )
  61. パブリックLocalContainerEntityManagerFactoryBean マスターEntityManagerFactory(){
  62. LocalContainerEntityManagerFactoryBean lb = 新しい LocalContainerEntityManagerFactoryBean();
  63. lb.setDataSource(マスターデータソース());
  64. lb.setPackagesToScan(
  65. 新しい文字列[]{MasterTenant.class.getPackage().getName()、MasterTenantRepository.class.getPackage().getName()}
  66. );
  67.  
  68. //名前の設定  Springが設定する永続ユニット場合 定義されていない場合は「default」になります
  69. lb.setPersistenceUnitName( "マスターデータベースの永続性ユニット" );
  70.  
  71. //Hibernate をJPA プロバイダーとして設定します。
  72. JpaVendorAdapter ベンダーアダプタ = 新しい HibernateJpaVendorAdapter();
  73. lb.setJpaVendorAdapter(ベンダーアダプタ)。
  74.  
  75. //休止状態のプロパティを設定する
  76. lb.setJpaProperties(hibernateProperties());
  77.  
  78. log.info( "masterEntityManagerFactory のセットアップに成功しました。" );
  79. lbを返します
  80. }
  81. @Bean(名前= "masterTransactionManager" )
  82. パブリックJpaTransactionManager マスタートランザクションマネージャー(@Qualifier( "masterEntityManagerFactory" )EntityManagerFactory emf){
  83. JpaTransactionManager トランザクションマネージャー = 新しい JpaTransactionManager();
  84. トランザクションマネージャ。EntityManagerFactory を設定します (emf)。
  85. log.info( "masterTransactionManager のセットアップに成功しました。" );
  86. transactionManagerを返します
  87. }
  88.  
  89. @ビーン
  90. パブリックPersistenceExceptionTranslationPostProcessor exceptionTranslationPostProcessor(){
  91. 新しい PersistenceExceptionTranslationPostProcessor()を返します
  92. }
  93. プライベートプロパティ hibernateProperties(){
  94. プロパティ properties = new Properties();
  95. プロパティを設定します(Environment.DIALECT、 "org.hibernate.dialect.MySQL5Dialect" );
  96. プロパティを設定します(Environment.SHOW_SQL, true );
  97. プロパティを設定します(Environment.FORMAT_SQL, true );
  98. properties.put(Environment.HBM2DDL_AUTO, "更新" );
  99. プロパティを返します
  100. }
  101. }

この構成クラスでは、主にパッケージ スキャン パス、エンティティ管理プロジェクト、トランザクション マネージャー、およびデータ ソース構成パラメーターの構成を提供します。

6. テナントビジネスモジュールを実装する

このセクションでは、SaaS 機能のデモンストレーションとして、テナント ビジネス モジュールのユーザー ログイン シナリオのみを提供します。実際、メイン レイヤー、ビジネス レイヤー、永続レイヤーは、通常の Spring Boot Web プロジェクトと変わりません。 SaaS アプリケーションのコードであることすら感じられません。

まず、ユーザー エンティティ User を作成します。ソース コードは次のようになります。

  1. @実在物
  2. @テーブル(名前= "USER" )
  3. @データ
  4. @NoArgsコンストラクタ
  5. @AllArgsコンストラクタ
  6. @ビルダー
  7. パブリッククラスUserはSerializableを実装します{
  8. プライベート静的最終ロングシリアルバージョンUID = -156890917814957041L;
  9.  
  10. @ID
  11. @(名前= "ID" )
  12. プライベート文字列ID;
  13.  
  14. @(名前= "ユーザー名" )
  15. プライベート文字列ユーザー名;
  16.  
  17. @(名前= "パスワード" )
  18. @サイズ(最小= 6、最大= 22、メッセージ = "ユーザー パスワードを入力する必要があります。長さは 6 から 22 の間です。" )
  19. プライベート文字列パスワード;
  20.  
  21. @(名前= "TENANT" )
  22. プライベート文字列テナント;
  23. }

ビジネス レイヤーは、ユーザー名に基づいてユーザー情報を取得するサービスを提供します。永続化レイヤー メソッドを呼び出して、ユーザー名に基づいてテナントのユーザー テーブルを取得します。条件を満たすユーザーレコードが見つかった場合、ユーザー情報が返されます。見つからない場合は null が返されます。永続層とビジネス層のソースコードは次のとおりです。

  1. @リポジトリ
  2. パブリックインターフェース UserRepository は JpaRepository< User ,String>、JpaSpecificationExecutor< User > を拡張します{
  3.  
  4. ユーザーfindByUsername(String username);
  5. }
  6. @Service( "ユーザーサービス" )
  7. パブリッククラス UserServiceImpl は UserService を実装します{
  8.  
  9. オートワイヤード
  10. プライベート UserRepository userRepository;
  11.  
  12. プライベート静的TwitterIdentifier 識別子 = 新しい TwitterIdentifier();
  13.  
  14.  
  15.  
  16. @オーバーライド
  17. パブリックvoid保存(ユーザー ユーザー) {
  18. ユーザー.setId(識別子.generalIdentifier());
  19. ユーザー.setTenant(TenantContextHolder.getTenant());
  20. userRepository.save(ユーザー);
  21. }
  22.  
  23. @オーバーライド
  24. 公共 ユーザーfindById(文字列 userId) {
  25. オプション<ユーザー> オプション = userRepository.findById(userId);
  26. if(オプション.isPresent()){
  27. オプションの.get()を返します
  28. }それ以外{
  29. 戻る ヌル;
  30. }
  31. }
  32.  
  33. @オーバーライド
  34. 公共 ユーザーfindByUsername(文字列 ユーザー名) {
  35. システム。出力.println(TenantContextHolder.getTenant());
  36. userRepository.findByUsername(ユーザー名)を返します
  37. }

ここでは、Twitter のスノーフレーク アルゴリズムを使用して ID ジェネレーターを実装します。

7. インターセプターを構成する

テナント識別子を取得するには、テナント情報インターセプターを提供する必要があります。ソース コードと構成インターセプターのソース コードは次のとおりです。

  1. /**
  2. * @author : Tan Chaohong (別名:ramostear)によって作成されました
  3. * @作成日時2019/5/26 0026-23:17
  4. * @修正する による
  5. * @以来:
  6. */
  7. 翻訳者
  8. パブリッククラス TenantInterceptor は HandlerInterceptor を実装します{
  9.  
  10. @オーバーライド
  11. public boolean preHandle(HttpServletRequest リクエスト、HttpServletResponse レスポンス、オブジェクト ハンドラ) 例外をスローします {
  12. 文字列テナント = request.getParameter( "テナント" );
  13. if(StringUtils.isBlank(テナント)){
  14. response.sendRedirect( "/login.html" );
  15. 戻る 間違い;
  16. }それ以外{
  17. テナントコンテキストホルダー。テナントを設定します。
  18. 戻る 真実;
  19. }
  20. }
  21. }
  22. @構成
  23. パブリッククラスInterceptorConfigはWebMvcConfigurationSupportを拡張します。
  24.  
  25. @オーバーライド
  26. 保護された void addInterceptors(InterceptorRegistry レジストリ) {
  27. registry.addInterceptor(新しい TenantInterceptor()).addPathPatterns( "/**" ).excludePathPatterns( "/login.html" );
  28. super.addInterceptors(レジストリ);
  29. }
  30. }

/login.html はシステムのログイン パスです。傍受者による傍受の範囲から除外する必要があります。そうしないと、ログインできなくなります。

8. 入居者の識別情報を維持する

ここでは、データ ソースを動的に設定するためのデータ サポートを提供するために、ThreadLocal を使用してテナント識別情報を保存しています。このクラスは、テナント ID の設定、テナント ID の取得、テナント ID のクリアを行う 3 つの静的メソッドを提供します。ソースコードは次のとおりです。

  1. パブリッククラスTenantContextHolder {
  2.  
  3. プライベート静的最終 ThreadLocal<String> CONTEXT = new ThreadLocal<>();
  4.  
  5. 公共 静的void setTenant(String テナント){
  6. コンテクスト。セット(テナント);
  7. }
  8.  
  9. 公共 静的文字列 getTenant(){
  10. CONTEXT.get()を返します
  11. }
  12.  
  13. 公共 静的voidクリア(){
  14. CONTEXT.remove();
  15. }
  16. }

これは、動的なデータ ソース設定を実装するための鍵となります。

9. 動的なデータソースの切り替え

動的なデータ ソースの切り替えを実装するには、CurrentTenantIdentifierResolver と AbstractDataSourceBasedMultiTenantConnectionProviderImpl の 2 つのクラスを使用する必要があります。名前からわかるように、1 つはテナント識別子を解析する役割を担い、もう 1 つはテナント識別子に対応するテナント データ ソース情報を提供する役割を担います。まず、テナント識別解決機能を完了するために、CurrentTenantIdentifierResolver インターフェースに resolveCurrentTenantIdentifier() メソッドと validateExistingCurrentSessions() メソッドを実装する必要があります。実装クラスのソースコードは次のとおりです。

  1. パッケージ com.ramostear.una.saas.tenant.config;
  2.  
  3. com.ramostear.una.saas.context.TenantContextHolder をインポートします。
  4. org.apache.commons.lang3.StringUtils をインポートします。
  5. org.hibernate.context.spi.CurrentTenantIdentifierResolver をインポートします。
  6.  
  7. /**
  8. * @author : Tan Chaohong (別名:ramostear)によって作成されました
  9. * @作成日時2019/5/26 0026-22:38
  10. * @修正する による
  11. * @以来:
  12. */
  13. パブリッククラスCurrentTenantIdentifierResolverImplはCurrentTenantIdentifierResolverを実装します。
  14.  
  15. /**
  16. * デフォルトのテナントID
  17. */
  18. プライベート静的最終文字列 DEFAULT_TENANT = "tenant_1" ;
  19.  
  20. /**
  21. * 現在のテナントのIDを解析する
  22. * @戻る 
  23. */
  24. @オーバーライド
  25. パブリック文字列resolveCurrentTenantIdentifier() {
  26. //テナント コンテキストを通じてテナント ID を取得します。この ID は、ユーザーがログインするときにヘッダーに設定されます。
  27. 文字列テナント = TenantContextHolder.getTenant();
  28. //コンテキスト内にテナントIDが見つからない場合は、デフォルトのテナントIDを使用するか、例外を直接報告します
  29. StringUtils.isNotBlank(tenant)?tenant:DEFAULT_TENANTを返します
  30. }
  31.  
  32. @オーバーライド
  33. パブリックブール値validateExistingCurrentSessions() {
  34. 戻る 真実;
  35. }
  36. }

このクラスのロジックは非常に単純で、ThreadLocalから現在設定されているテナント識別子を取得することです。

テナント識別子解決クラスを作成したら、テナント データ ソース プロバイダー クラスを拡張して、データベースからテナント データ ソース情報を動的に照会する必要があります。ソースコードは次のとおりです。

  1. 翻訳者
  2. @構成
  3. パブリッククラス DataSourceBasedMultiTenantConnectionProviderImpl は AbstractDataSourceBasedMultiTenantConnectionProviderImpl を拡張します{
  4.  
  5. プライベート静的最終ロングシリアルバージョンUID = -7522287771874314380L;
  6. オートワイヤード
  7. プライベートマスターテナントリポジトリマスターテナントリポジトリ;
  8.  
  9. プライベート Map<String,DataSource> dataSources = new TreeMap<>();
  10.  
  11. @オーバーライド
  12. 保護されたデータソース selectAnyDataSource() {
  13. データソースが空の場合(){
  14. リスト<MasterTenant> テナント = masterTenantRepository.findAll();
  15. テナント.forEach(masterTenant->{
  16. データソースを put (masterTenant.getTenant()、DataSourceUtils.wrapperDataSource(masterTenant));
  17. });
  18. }
  19. データソースを返します()。イテレータ()。();
  20. }
  21. @オーバーライド
  22. 保護されたデータソース selectDataSource(String テナント) {
  23. if(!dataSources.containsKey(テナント)){
  24. リスト<MasterTenant> テナント = masterTenantRepository.findAll();
  25. テナント.forEach(masterTenant->{
  26. データソースを put (masterTenant.getTenant()、DataSourceUtils.wrapperDataSource(masterTenant));
  27. });
  28. }
  29. dataSources.get(テナント)を返します
  30. }
  31. }

このクラスでは、テナント データ ソース ライブラリを照会してテナント データ ソース情報を動的に取得し、テナント ビジネス モジュールのデータ ソース構成にデータ サポートを提供します。

最後に、プロジェクト全体の中核となるテナント ビジネス モジュール データ ソース構成も提供する必要があります。コードは次のとおりです。

  1. 翻訳者
  2. @構成
  3. @トランザクション管理を有効にする
  4. @ComponentScan(ベースパッケージ = {
  5. "com.ramostear.una.saas.tenant.model" ,
  6. 「com.ramostear.una.saas.tenant.repository」  
  7. })
  8. @EnableJpaRepositories(ベースパッケージ = {
  9. 「com.ramostear.una.saas.tenant.repository」
  10. 「com.ramostear.una.saas.tenant.service」  
  11. },entityManagerFactoryRef = "テナントエンティティマネージャーファクトリ"  
  12. 、transactionManagerRef = "tenantTransactionManager" )
  13. パブリッククラス TenantDataSourceConfig {
  14.  
  15. @Bean( "jpaベンダーアダプタ" )
  16. パブリックJpaVendorAdapter jpaVendorAdapter(){
  17. 新しい HibernateJpaVendorAdapter()を返します
  18. }
  19. @Bean(名前= "tenantTransactionManager" )
  20. パブリックJpaTransactionManager トランザクションマネージャー (EntityManagerFactory エンティティマネージャーファクトリー){
  21. JpaTransactionManager トランザクションマネージャー = 新しい JpaTransactionManager();
  22. トランザクションマネージャ。EntityManagerFactory を設定します。
  23. transactionManagerを返します
  24. }
  25.  
  26. @Bean(名前= "データソースベースのマルチテナント接続プロバイダー" )
  27. @ConditionalOnBean(名前= "masterEntityManagerFactory" )
  28. パブリックMultiTenantConnectionProvider multiTenantConnectionProvider(){
  29. 新しい DataSourceBasedMultiTenantConnectionProviderImpl()を返します
  30. }
  31. @Bean(名前= "currentTenantIdentifierResolver" )
  32. パブリックCurrentTenantIdentifierResolver currentTenantIdentifierResolver(){
  33. 新しい CurrentTenantIdentifierResolverImpl()を返します
  34. }
  35.  
  36. @Bean(名前= "tenantEntityManagerFactory" )
  37. @ConditionalOnBean(名前= "データソースベースのマルチテナント接続プロバイダー" )
  38. パブリックLocalContainerEntityManagerFactoryBean エンティティマネージャファクトリ(
  39. @Qualifier( "datasourceBasedMultiTenantConnectionProvider" )MultiTenantConnectionProvider 接続プロバイダー、
  40. @Qualifier( "currentTenantIdentifierResolver" )現在のテナント識別子リゾルバー テナント識別子リゾルバー
  41. ){
  42. LocalContainerEntityManagerFactoryBean を新規作成します。
  43. ローカルBean.setPackagesToScan()
  44. 新しい文字列[]{
  45. ユーザー.class.getPackage().getName()、
  46. UserRepository.class.getPackage().getName()、
  47. UserService.class.getPackage().getName()
  48.  
  49. }
  50. );
  51. ローカルBeanにJpaVendorAdapterを設定します(jpaVendorAdapter());
  52. localBean.setPersistenceUnitName( "テナントデータベース永続ユニット" );
  53. Map<String,Object> プロパティ = new HashMap<>();
  54. プロパティを設定します( Environment.MULTI_TENANT、 MultiTenancyStrategy.SCHEMA )。
  55. properties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER、接続プロバイダー);
  56. プロパティを設定します。
  57. プロパティを設定します(Environment.DIALECT、 "org.hibernate.dialect.MySQL5Dialect" );
  58. プロパティを設定します(Environment.SHOW_SQL, true );
  59. プロパティを設定します(Environment.FORMAT_SQL, true );
  60. properties.put(Environment.HBM2DDL_AUTO, "更新" );
  61. localBean.setJpaPropertyMap(プロパティ);
  62. localBeanを返します
  63. }
  64. }

この構成ファイルの内容のほとんどは、メイン データ ソースの構成と同じです。唯一の違いは、テナント識別子リゾルバとテナント データ ソース補充ソースの設定です。これにより、データベース操作コマンドを実行する前にどのようなデータベース接続情報を設定する必要があるか、およびユーザー名やパスワードなどの情報が Hibernate に通知されます。

10. アプリケーションテスト

最後に、このコースでは、簡単なログインケースを通じて SaaS アプリケーションをテストします。これを行うには、ユーザー ログイン ロジックを処理するコントローラーを提供する必要があります。この場合、ユーザー パスワードは厳密に暗号化されず、プレーン テキストで比較されます。権限認証フレームワークは提供されていません。 SaaS の基本機能が利用可能かどうかを確認します。ログイン コントローラ コードは次のとおりです。

  1. /**
  2. * @author : Tan Chaohong (別名:ramostear)によって作成されました
  3. * @作成日時2019/5/27 0027-0:18
  4. * @修正する による
  5. * @以来:
  6. */
  7. @コントローラ
  8. パブリッククラスLoginController {
  9.  
  10. オートワイヤード
  11. プライベート UserService userService;
  12.  
  13. @GetMapping( "/login.html" )
  14. パブリック文字列ログイン(){
  15. 戻る  "/ログイン" ;
  16. }
  17.  
  18. @PostMapping( "/ログイン" )
  19. パブリック文字列ログイン(@RequestParam( name = "username" )文字列ユーザー名、@RequestParam( name = "password" )文字列パスワード、ModelMapモデル){
  20. システム。出力.println( "テナント:" +TenantContextHolder.getTenant());
  21. ユーザー ユーザー= userService.findByUsername(ユーザー名);
  22. if(ユーザー!= null ){
  23. if(ユーザー.getPassword().equals(パスワード)){
  24. model.put( "ユーザー" ユーザー);
  25. 戻る  "/索引" ;
  26. }それ以外{
  27. 戻る  "/ログイン" ;
  28. }
  29. }それ以外{
  30. 戻る  "/ログイン" ;
  31. }
  32. }
  33. }

プロジェクトを開始する前に、テナント データ ソース情報を保存するためのメイン データ ソースに対応するデータベースとデータ テーブルを作成する必要があります。また、テナント ビジネス データを格納するためのテナント ビジネス モジュール データベースとデータ テーブルも提供する必要があります。すべての準備ができたら、プロジェクトを起動し、ブラウザにhttp://localhost:8080/login.htmlと入力します。

ログイン ウィンドウに対応するテナント名、ユーザー名、パスワードを入力して、ホームページに正常にアクセスできるかどうかをテストします。さらにいくつかのテナントとユーザーを追加して、ユーザーが対応するテナントに正常に切り替えられるかどうかをテストできます。

要約する

ここでは、Spring Boot + JPA を使用してマルチテナント アプリケーションを迅速に実装する方法を紹介します。この方法では、SaaS アプリケーション プラットフォームを実装するための最もコアとなる技術的手段のみが使用されます。完全で使用可能なプロジェクト コードではありません。たとえば、ユーザー認証と承認はこの記事では取り上げません。

<<:  Kubernetesネットワークポリシーについてはこの記事で十分です

>>:  RongCloudのリアルタイムオーディオとビデオは完全にアップグレードされ、統合されたコミュニケーションソリューションを実現します。

推薦する

電子商取引のプロモーション用のインタラクティブ ゲームを設計するにはどうすればよいでしょうか?

01Eコマースプロモーション大プロモーションステージ618やダブル11などの大規模な電子商取引プロモ...

オリジナルウェブサイトのSEO: ウェブサイトの外部リンクについて

前回、「SEOの忘れられた基礎」の中で、外部リンクの役割について触れました。今日は、ウェブサイトの最...

QingCloudストレージが全面的にアップグレードされ、自社開発のQingStor U10000がさらなるデータ容量を解放

自動運転車をフル稼働でテストすると、毎日数テラバイト、あるいは数十テラバイトのデータが生成されること...

ドロワーナビゲーションにより、ユーザーはコア機能に集中できます

[編集者注] この記事は@kentzhuの個人ブログから転載したものです。ナビゲーションはプロダクト...

中国におけるSEOの持続的発展について

中国の SEO が直面している問題:プロのSEO専門家の不足SEOチームの不足SEOガイダンスの概念...

ウェブサイトを最適化するときは冷静さを保つことが重要です。

ウェブサイト運営者、特に中小企業やウェブマスターはウェブサイトのランキングを毎日チェックしており、少...

インデックスされたウェブサイトの総数はサイトからのものではないことに注意してください

SEOERとして、私たちは日々、担当しているサイトのさまざまなデータの変化に注目しています。最も重要...

#黑5# raksmart: VPS半額、無制限のCN2サーバー、10Gbpsの帯域幅 Du Fu

Raksmart データセンターは、感謝祭とブラックフライデーに以下のプロモーションを実施しています...

新年の始まりにピッタリの注目アイテム! Sangfor、エンタープライズクラスの分散ストレージaStor-EDSを発売

1月20日、Sangforの第11回コアパートナーフォーラムが海南省三亜で開催された。会議のメインフ...

SEO 作業で PDCA を実施していますか?

私はSEOに2年以上携わっています。通常の仕事のプロセスでは、専門知識の学習と専門スキルの習得に加え...

Skywalking 分散リンク トレーシングの概要

今日は、分散リンクトラッキングソフトウェアを紹介します。リンクトラッキングを導入する必要があるのはな...

ハイブリッドクラウドとマルチテナントのビッグデータプラットフォームの容量とコンプライアンスに関する考慮事項

導入近年、データに基づく洞察の重要性が徐々に認識されるようになりました。このアプローチは、戦略的な意...

華雲が支援する上洋ハイブリッドクラウドがCAICTのハイブリッドクラウド優秀事例賞を初受賞

11月17日、中国情報通信研究院(CAICT)が主導し、オープンソース・アライアンス・フォー・クラウ...