本連載では、以下に示すようなマイクロサービスアーキテクチャのアプリケーション環境を構築しています。

構成図

前回はSpringSecutiyを使ったさまざまなカスタム設定を行ってWebアプリケーションのログインページを実装し、実際にアプリケーションを起動してみました。今回は、バックエンドにユーザー情報をリソースとして取り扱うマイクロサービスを作成します。

なお、本連載で実際に作成するアプリケーションはGitHub上にコミットしています。ただし、前回の実装とはブランチを変更しているので、参照する際は留意してください。以降に記載するソースコードでは、import文など本質的でない記述を省略している部分があるので、実行コードを作成する際は、必要に応じて適宜GitHubにあるソースコードも参照してください。

バックエンドマイクロサービスの実装

バックエンドサブネットに構築するマイクロサービスですが、連載「AWSで実践!基盤・デプロイ自動化」の第4回と同様のプロジェクトパッケージ構成で、シンプルかつRESTfulなアプリケーションとして作成します。マイクロサービスから共通的に利用するcommonプロジェクトも合わせて作成し、Mavenのpom.xmlから参照設定を行います。

アプリケーションコンポーネントのレイヤ構成は第4回と同様、TERAOLUNAのガイドライン「アプリケーションのレイヤ化」の考え方に準拠します。今回実装する、ユーザーリソースを返却するバックエンドマイクロサービスのコンポーネント構成は以下の通りです。

コンポーネント 説明 必須
App SpringBootアプリケーションを実行する起動クラス
MvcConfig SpringMVCの設定を行うクラス
JpaConfig JPAおよびSpringDataJPAの設定クラス
DomainConfig ServiceやRepositoryなどドメイン層の設定クラス
DevConfig プロファイル「dev」におけるHSQLの環境構築やデータソース接続設定クラス
BackendUserController ユーザーリソースを返すRestController
XxxxResourceMapper リソースとエンティティクラスを変換するMapper
SampleService(Impl) データベースへアクセスするRepositoryなどを使ってユーザーエンティティクラスを返却するサービスクラス
UserRepository ユーザーテーブルへアクセスするためのSpringDataJPAにおけるRepositoryインタフェースクラス
User ユーザーを表すエンティティクラス
Credential/CredentialPK 認証を表すエンティティクラスおよびプライマリキークラス

では早速、今回の実装でポイントとなる箇所を説明していきましょう。今回は、ローカル環境で起動するアプリケーションをプロファイル「dev」で実行する構成とします。devプロファイル環境では、インメモリDBであるHSQLを利用します。

package org.debugroom.mynavi.sample.aws.microservice.backend.user.config;

// omit

@Profile("dev")
@Configuration
public class DevConfig {

    @Bean
    public DataSource dataSource(){
        return (new EmbeddedDatabaseBuilder())
             .setType(EmbeddedDatabaseType.HSQL)
             .addScript("classpath:schema-hsql.sql")
             .addScript("classpath:data-hsql.sql")
             .build();
    }
}

アプリケーション内のデータベースアクセスにはSpring Data JPAを用いています。本稿では詳細な解説は行いませんが、必要に応じて 連載「AWSで作るクラウドネイティブアプリケーションの基本」のデータベースアクセス(JPA編)」を参照してください。

なお、返却するユーザーリソースの元になるUserエンティティと、それと1対多の関係を持つCredentialエンティティは、以下のようなプロパティを持つクラスとします。

package org.debugroom.mynavi.sample.aws.microservice.backend.user.domain.model.entity;

// omit

@Entity
@Table(name = "usr", schema = "public", catalog="sample")
public class User implements Serializable{

    private long userId;
    private String firstName;
    private String familyName;
    private String loginId;
    private Boolean isLogin;
    private Boolean isAdmin;
    private Integer ver;
    private Timestamp lastUpdatedAt;
    private Set<Credential> credentialsByUserId;

    // omit
package org.debugroom.mynavi.sample.aws.microservice.backend.user.domain.model.entity;

// omit

@Entity
@IdClass(CredentialPK.class)
public class Credential {

    private long userId;
    private String credentialType;
    private String credentialKey;
    private Timestamp validDate;
    private Integer ver;
    private User usrByUserId;

    // omit

BackendUserControllerクラスでは、以下のようにエンドポイントを持つREST API構成とします。なお、JSONで返却されるリソースクラスは、バックエンドのマイクロサービスやフロントエンドのWebアプリケーション双方で参照できるように、commonプロジェクト内に作成します。

No エンドポイント 説明
1 /backend/user/api/v1/users データベースにあるユーザーリソースを全て返却するAPI
2 /backend/user/api/v1/users/X Xで指定されたユーザーIDを持つユーザーリソースを返却するAPI
3 /backend/user/api/v1/users/user?loginId=Y yで指定されたログインID情報を持つユーザーリソースを返却するAPI

このAPIを構成するためのController実装やアプリケーション設定ファイル(application.yml)は以下の通りです。

package org.debugroom.mynavi.sample.aws.microservice.backend.user.app.web;

// omit

import org.debugroom.mynavi.sample.aws.microservice.common.apinfra.exception.BusinessException;
import org.debugroom.mynavi.sample.aws.microservice.common.model.UserResource;

@RestController
@RequestMapping("api/v1")
public class BackendUserController {

    @Autowired
    SampleService sampleService;

    @GetMapping("/users")
    public List<UserResource> getUsers(){
        return UserResourceMapper.mapWithCredentials(sampleService.getUsers());
    }

    @GetMapping("/users/{id:[0-9]+}")
    public UserResource getUser(@PathVariable Long id) throws BusinessException {
        return UserResourceMapper.mapWithCredentials(sampleService.getUser(id));
    }

    @GetMapping("/users/user")
    public UserResource getUserByLoginId(
      @RequestParam("loginId") String loginId) throws BusinessException {
        return UserResourceMapper.mapWithCredentials(
          sampleService.getUserByLoginId(loginId));
    }

}
server:
  servlet:
    context-path: /backend/user

application.ymlの「server.servlet.context-path」で指定している「/backend/user」は、アプリケーションのURLドメイン名の配下に指定されるコンテキストパスになります。これを指定するのは、複数のマイクロサービスが実行される場合に、ALBでターゲットグループを指定する際の識別子として利用するためです。

また、今回は実装していませんが、APIのバージョン変更が発生していく際は@ProfileアノテーションなどSpringのプロファイル機能を使って、バージョン情報を指定して実行するようにすると良いでしょう。実行されるコンテナ内のマイクロサービスアプリケーションのControllerをプロファイルで切り替えつつ、ターゲットグループに「/backend/user/api/vX」といったかたちでバージョンごとに異なるコンテナへルーティングされるように構成すれば、複数のAPIバージョンを持つアプリケーションコンテナが混在する場合でも同一のECSクラスタ内で並行して実行でき、より柔軟性の高い運用が可能になります。この辺りについては、またALBやターゲットグループを作成するときに解説します。

なお、マイクロサービス内で発生した例外は、commonプロジェクト内に作成した「@ControllerAdviceを付与したExceptionHanlder」を使って一元的に設定することが可能です。

今回の実装では、想定されるビジネス例外やチェックエラーは、必要なエラーステータスコードを設定して返却するものとします。システム例外となるRuntimeException系のエラーは、Springが自動的に500系のInternalServerErrorでレスポンスするので設定はしていません。

package org.debugroom.mynavi.sample.aws.microservice.common.apinfra.exception;

import java.util.List;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class CommonExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(
      BusinessException businessException){
        businessException.setStackTrace(new StackTraceElement[]{});
        return new ResponseEntity<>(
          BusinessExceptionResponse.builder()
                  .businessException(businessException).build(), HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
    public ResponseEntity<ErrorResponse> handleValidationException(Exception exception){
        BindingResult bindingResult = null;
        if(exception instanceof MethodArgumentNotValidException){
            bindingResult = ((MethodArgumentNotValidException)exception).getBindingResult();
        }else if(exception instanceof BindException){
            bindingResult = ((BindException)exception).getBindingResult();
        }
        List<FieldError> fieldErrors = bindingResult.getFieldErrors();
        return new ResponseEntity<>(
          ValidationErrorResponse.builder().validationErrors(
                  ValidationErrorMapper.map(fieldErrors)).build(),
          HttpStatus.BAD_REQUEST);
    }
}

アプリケーションをメインクラスから起動した後、エンドポイントにアクセスすると以下のような応答が返されます。

curl --dump-header - http://localhost:8081/backend/user/api/v1/users/user?loginId=jiro.mynavi

HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 21 Aug 2020 16:53:47 GMT

{"userId":"2","firstName":"jiro","familyName":"mynavi","loginId":"jiro.mynavi","credentialResources":[{"userId":2,"credentialType":"PASSWORD","credentialKey":"$2a$11$5knhINqfA8BgXY1Xkvdhvu0kOhdlAeN1H/TlJbTbuUPDdqq.H.zzi","validDate":"2018-12-31T15:00:00.000+00:00"},{"userId":2,"credentialType":"ACCESSTOKEN","credentialKey":"987654321poiuytrewq","validDate":"2015-12-31T15:00:00.000+00:00"}],"login":false,"admin":false}

指定したログインIDを持つユーザーが存在しないなど、想定されるビジネス例外の場合は以下のようなレスポンスが返却されます。

curl --dump-header - http://localhost:8081/backend/user/api/v1/users/user?loginId=jiro.mynavi2

HTTP/1.1 400
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 20 Aug 2020 19:31:43 GMT
Connection: close

{"@class":"org.debugroom.mynavi.sample.aws.microservice.common.apinfra.exception.BusinessExceptionResponse","businessException":{"cause":null,"stackTrace":[],"code":"BE0002","args":null,"message":"ログインIDは存在しません。 LoginID : jiro.mynavi2","suppresslizedMessage":"ログインIDは存在しません。 LoginID : jiro.mynavi2"}}

実際にはHTTPステータスコードで404指定し、「リソースが存在しない」などで返す方法が妥当なケースもありますが、複雑な理由によるビジネス例外やレスポンスにエラー内容を表すパラメータが必要な場合を想定して、今回はJSONでBusinessExceptionを返す形式で実装しています。お手元で実装する際は、APIの規約に応じて、適切なレスポンスステータスコードを返却するようにしてください。

* * *

今回はデータベースに保存したユーザー情報を取得するバックエンドマイクロサービスを作成しました。次回は、前回作成したフロントのWebアプリケーションからこのマイクロサービスを呼び出すようなかたちにして、データベースに保存されたユーザー情報を使って正しく認証処理が行われるように実装し直していきます。

著者紹介


川畑 光平(KAWABATA Kohei) - NTTデータ

金融機関システム業務アプリケーション開発・システム基盤担当、ソフトウェア開発自動化関連の研究開発を経て、デジタル技術関連の研究開発・推進に従事。

Red Hat Certified Engineer、Pivotal Certified Spring Professional、AWS Certified Solutions Architect Professional等の資格を持ち、アプリケーション基盤・クラウドなど様々な開発プロジェクト支援にも携わる。AWS Top Engineers & Ambassadors選出。

本連載の内容に対するご意見・ご質問は Facebook まで。