developers

Spring Boot API で DTO をエンティティへ自動的にマッピングする

ModelMapper が Spring Boot API 上のエンティティへの DTO のマッピング プロセス自動化にどのように役立つか学びましょう。

Sep 5, 20178 min read

DTO とは何か?

DTO はデータ転送オブジェクトを表し、リモート インターフェイスで作業しているときに、呼び出し数を減らすことを思いついた設計パターンです。Martin Fowler がブログで定義するように、データ転送オブジェクトを使用する主な理由は複数のリモート通話を1つのバッチにすることです。

例えば、銀行アカウントのデータを公開する RESTful API で通信しているとしましょう。この場合、現状や最新のアカウント取引をチェックするように複数の要求を発する代わりに、銀行は DTO を返すエンドポイントを公開してすべてを要約することができます。リモート アプリケーションの操作で最も高価なもののひとつはクライアントとサーバーの間の往復時間なので、この粒度の粗いインターフェイスはパフォーマンスの改善に大きく役立ちます。

DTO および Spring Boot API

Java(と Spring Boot)で書き込まれた RESTful API で DTO を使用するもうひとつの利点はドメイン オブジェクト(別名:エンティティ)の実装詳細を非表示するときに役立ちます。エンドポイントを介してエンティティを公開することは、どのプロパティをどの操作を通して変更するかをよく注意しなければ、セキュリティ問題になります。

例として、ユーザーの詳細を公開し、2つのエンドポイントを介してユーザーの更新を承諾する Java API を想像してみましょう。最初のエンドポイントは

GET
リクエストを処理し、ユーザーデータを返します。そして、2つめのエンドポイントがこれらの詳細を更新するために
PUT
リクエストを承諾します。このアプリケーションが DTO を利用しなければ、ユーザーのすべてのプロパティは最初のエンドポイント(例:パスワード)で公開され、2つめのエンドポイントはユーザーを更新するときにどのプロパティを承諾するかを精選しなければなりません(例:誰もがユーザーの役割を更新できるわけではありません)。この状況を乗り越えるために、DTO は最初のエンドポイントが対象とするものだけを公開し、2つめのエンドポイントが承諾するものを制限するのに役に立ちます。この特性はアプリケーション内のデータ整合性を保つのに役立ちます。

DTO は Java アプリケーション上のデータ整合性を保つのに役立ちます。

これをツイートする

本アーティクルでは、このような状況を処理するために DTO を利用していきます。後で説明するように、この設計パターンではさらにいくつかのクラスをアプリケーションに導入しますが、そのセキュリティは改善されます。

ModelMapper の導入

DTO をマップするために面倒な/定型コードをエンティティに書き込んだり、その反対を避けるために、ModelMapper と呼ばれるライブラリを使用していきます。ModelMapper の目標はあるオブジェクト モデルを別のものにどのようにマップするかを自動的に決定して、オブジェクト マッピングを簡単にすることです。このライブラリはかなり強力で、マッピングプロセスを簡素化する非常にたくさんの構成を適用できますが、ほとんどのケースに当てはまる既定の動作を提供して構成よりも規則を優遇します。

このライブラリのユーザー マニュアルはよく書かれており、マッピング プロセスを調整する必要があるときに貴重なリソースになります。このライブラリの機能を少しご紹介するために、次のような

User
があるとしましょう。

// assume getters and setters
class User {
  long id;
  String firstName;
  String lastName;
  String email;
  String password;
  String securitySocialNumber;
  boolean isAdmin;
}

そして、

id
firstName
、および
email
のみを公開するとします。ModelMapper を使って、次のように DTO を生成しなければなりません。

// assume getters and setters
class UserDTO {
  long id;
  String firstName;
  String email;
}

それから、次ように ModelMapper を呼び出します。

ModelMapper modelMapper = new ModelMapper();
// user here is a prepopulated User instance
UserDTO userDTO = modelMapper.map(user, UserDTO.class);

つまり、公開したい構成を定義し、

modelMapper.map
を呼び出すことで、目標を達成し、公開したくないものを非表示にします。Jackson のようなライブラリはオブジェクトをシリアライズするときにプロパティの一部を無視する注釈を提供することに反対する方がいますが、このソリューションはデベロッパーがエンティティを高速化する唯一の方法を制限します。DTO と ModelMapper を使用することで、希望するだけの異なるバージョン(と異なる構造)のエンティティを提供できます。

何を構築するか?

これからは、Spring Boot RESTful API のエンティティを公開する DTO を使うことに集中していきます。ModelMapper を使って、この API を DTO に、およびその反対に作成するエンティティからマップしていきます。新しいプロジェクトを一から設定することにあまり時間を費やしたくはないので、前回のアーティクルで作成した QuestionMarks プロジェクトを利用していきます。このアーティクルの全文を読む 必要はありません 。これからそのプロジェクトをサポートする GitHub レポジトリを複製し、私たちが関心があることを中心とした確固たる基盤を与えてくれる特定の Git タグ を確認していきます。

QuestionMarks の背景にある考え方は、このアプリケーションはユーザーが多岐選択式の質問に答えて実践し、知識を高めることができることです。より良い体制を提供するために、これらの質問は異なる試験にグループ化されています。例えば、ユーザーがインタビューに備えるのに役立つ JavaScript 関係の質問を保留する JavaScript インタビュー と呼ばれる試験があります。もちろん、本書では、アプリケーション全体を構築すると時間がかかり、アーティクルが大きくなりますので、構築しませんが、上記のテクノロジーのアクションをご覧いただけます。

前回のアーティクルでは、Spring Data JPA、PostgreSQL、および Liquibase を統合して永続レイヤーを管理しました。エンティティを公開する良い方法がなかったので、RESTful エンドポイントを作成しませんでした。これは本書の主な目標です。

PostgreSQL を起動する

既存のプロジェクトを複製する前に、PostgreSQL インスタンスを設定してデータベース操作や永続化をサポートする必要があります。前のアーティクルで述べたように、Docker は開発機にインストールしないでアプリケーションを起動するには素晴らしいソリューションです。

Docker をインストールする必要がありますが、それをインストールするプロセスは簡単です(MacOS はこちらのリンクWindows はこちらのリンク、および Ubuntu はこちらのリンク)。Docker を正しくインストールすると、Docker 化した PostgreSQL のインスタンスを次のように実行できます。

docker run --name questionmarks-psql \
    -p 5432:5432 \
    -e POSTGRES_DB=questionmarks \
    -e POSTGRES_PASSWORD=mysecretpassword \
    -d postgres

Docker インスタンス内で PostgreSQL を起動したくない場合、または別の PostgreSQL インスタンスがすでにある場合は、

questionmarks
と呼ばれるデータベースがそれにあり、
postgres
ユーザーがパスワードとして
mysecretpassword
を持っていることを保証する必要があります。または、
./src/main/resources/application.properties
ファイルのこれら値を変更します。

spring.datasource.url=jdbc:postgresql://localhost/questionmarks
spring.datasource.username=postgres
spring.datasource.password=mysecretpassword
spring.datasource.driver-class-name=org.postgresql.Driver

QuestionMarks を複製する

次のステップは、QuestionMarks をサポートする GitHub レポジトリ を複製することです。本書の特定タグを確認してください。次のコマンドを発行して達成します。

git clone https://github.com/auth0-blog/questionmarks-server.git
cd questionmarks-server
git checkout post-2

前のアーティクルではエンドポイントを作成しなかったので、ここでアプリケーションを実行するのは良くありません。アプリケーションを実行しても害にはなりません。Liquibase はすでに作成されている5つのエンティティをサポートするテーブルの構造を作成します。しかし、エンドポイントを開発した後にそれが実行するのを待つことは同じ効果を出します。

その後、Spring Boot プロジェクトを優先 IDE(統合開発環境)にインポートする必要があります。

依存関係を追加する

QuestionMarks プロジェクトを複製し、IDE にインポートしたら、DTO の自動マッピング処理に進みます。まず行う最初のステップは

./build.gradle
ファイルに依存関係として ModelMapper を追加します。依存関係を
hibernate-java8
library
にも追加します。このアーティクルを使って、Java8-固有クラスをデータベース上の列にマップします。

// ... other definitions

dependencies {
    // ... other dependencies
  compile('org.modelmapper:modelmapper:1.1.0')
  compile('org.hibernate:hibernate-java8:5.1.0.Final')
}

試験エンティティをリファクタ―する

DTO を使う本当の利点を証明し、マッピング プロセスのさらに有意義な例を実行するために、

Exam
エンティティを少しリファクターしていきます。その試験がいつ作成され、最後に編集された期日を把握するために、2つのデータ プロパティをそれに追加していきます。そして、それが発行された(一般が利用可能)か否かを示すフラッグを追加していきます。
./src/main/java/com/questionmarks/model/Exam.java
ファイルを開き、次のコードの行を追加します。

// ... other imports
import java.time.LocalDateTime;

// ... annotations
public class Exam {
    // ... other properties

    @NotNull
    private LocalDateTime createdAt;

    @NotNull
    private LocalDateTime editedAt;

    @NotNull
    private boolean published;
}

最後のセクションに

hibernate-java8
ライブラリをインポートしないと、JPA/Hibernate は自動的に
LocalDateTime
をデータベースにマップすることはできませんので、ご注意ください。幸運なことにこのライブラリはユーザーを助けるためにあります。そうでなければ、独自のコンバーターを作る必要があります。

また、新しいプロパティ(列として)を、アプリケーションをサポートする PostgreSQL データベースに追加する必要があります。前回のアーティクルでは、スキーマ移行を処理する Liquibase をセットアップしましたので、そのコマンドで新しいファイルを作り、新しい列を追加しなければなりません。このファイルを

v0002.sql
と呼び、次のコンテンツでそれを
./src/main/resources/db/changelog/changes/
フォルダに追加します。

alter table exam
  add column created_at timestamp without time zone not null default now(),
  add column edited_at timestamp without time zone not null default now(),
  add column published boolean not null default false;

このアプリケーションを次回実行するとき、Liquibase はこのファイルを読み取り、これらのコマンドを実行して3つの列を追加します。SQL コマンドは既存記録の既定値でこれら列も事前設定します。そのほかに、JPA/Hibernate が列やその処理に対応するために変更する必要があるものはありません。

DTO を作成する

ユーザーに直接変更してほしくない機微なプロパティを保留する

Exam
エンティティを変更したので、ユーザーのリクエストを良く処理するために2つの DTO を作成していきます。最初の DTO は新しい試験の作成を担当するので、
ExamCreationDTO
と呼びます。
com.questionmarks.model
パッケージ内の
dto
と呼ばれる新しいパッケージにこの DTO クラスを作成します。このクラスには次のソースコードが含まれます。

package com.questionmarks.model.dto;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

@Getter
@Setter
public class ExamCreationDTO {
    @NotNull
    private String title;

    @NotNull
    private String description;

    @JsonIgnore
    private final LocalDateTime createdAt = LocalDateTime.now();

    @JsonIgnore
    private final LocalDateTime editedAt = LocalDateTime.now();
}

新しい試験の作成に協力的なユーザーは新しい DTO で定義されている構造を含むリクエストを送信する必要があります。つまり、このユーザーは

title
description
を正確に送信する必要があります。
createdAt
プロパティと
editedAt
プロパティの両方は DTO 自体によって事前設定されます。これらプロパティを介して値を送信しようとするユーザーがあるときは、このアプリケーションは
@JsonIgnore
のマークがついているのでそれらを無視します。そのほかに、
Exam
エンティティに追加した
published
プロパティは DTO がそれを含まなかったので、外部からは完全に非表示にされます。

これから作成する2つめの DTO は既存の試験を更新する担当になります。この DTO を

ExamUpdateDTO
と呼び、それを次のコードで
com.questionmarks.model.dto
パッケージに含みます。

package com.questionmarks.model.dto;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.Id;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

@Getter
@Setter
public class ExamUpdateDTO {
    @Id
    @NotNull
    private Long id;

    @NotNull
    private String title;

    @NotNull
    private String description;

    @JsonIgnore
    private final LocalDateTime editedAt = LocalDateTime.now();
}

他の DTO との違いは、これは更新する

id
プロパティを含み、このフィールドを更新するのは意味がないので、
createdAt
プロパティがありません。

DTO の視点から、これは大体、試験を安全に作成し、更新するために必要なことです。これから、これらマッピングを手動で操作しなくてもいいように、DTO をエンティティへマッピングするプロセスの合理化に集中していきます。

ちょっとお待ちください!次の作業に進む前に、ModelMapper が実際に DTO を

Exam
エンティティへマッピングできることを保証する小さな単体テストを作りましょう。(
./src/test/java/com/questionmarks/
フォルダ内の)テストコードにある
com.questionmarks
パッケージ内に
model
と呼ばれる新しいパッケージを作りましょう。 それから、次のコード内に
ExamUT
と呼ばれるクラスを作ります。

package com.questionmarks.model;

import com.questionmarks.model.dto.ExamCreationDTO;
import com.questionmarks.model.dto.ExamUpdateDTO;
import org.junit.Test;
import org.modelmapper.ModelMapper;

import static org.junit.Assert.assertEquals;

public class ExamUT {
    private static final ModelMapper modelMapper = new ModelMapper();

    @Test
    public void checkExamMapping() {
        ExamCreationDTO creation = new ExamCreationDTO();
        creation.setTitle("Testing title");
        creation.setDescription("Testing description");

        Exam exam = modelMapper.map(creation, Exam.class);
        assertEquals(creation.getTitle(), exam.getTitle());
        assertEquals(creation.getDescription(), exam.getDescription());
        assertEquals(creation.getCreatedAt(), exam.getCreatedAt());
        assertEquals(creation.getEditedAt(), exam.getEditedAt());

        ExamUpdateDTO update = new ExamUpdateDTO();
        update.setTitle("New title");
        update.setDescription("New description");

        modelMapper.map(update, exam);
        assertEquals(update.getTitle(), exam.getTitle());
        assertEquals(update.getDescription(), exam.getDescription());
        assertEquals(creation.getCreatedAt(), exam.getCreatedAt());
        assertEquals(update.getEditedAt(), exam.getEditedAt());
    }
}

このクラスで唯一定義された

@Test
は特定の
title
description
ExamCreationDTO
のインスタンスを作り、新しい
Exam
を生成する
ModelMapper
のインスタンスを使用します。それから、この
Exam
ExamCreationDTO
によって保留されるものと同じ
title
description
createdAt
、および
editedAt
値を含むかを確認します。

最後に、

ExamUpdateDTO
のインスタンスを作成し、
title
description
、および
editedAt
プロパティが更新されたか、
createdAt
プロパティが変更されないままかを確認する前に作成された
Exam
インスタンスに適用します。ここで、IDE または
gradle test
コマンドを介してテストを実行すると、良い結果が見られるはずです。これから、エンジンの残りを構築し、DTO をエンティティへマップしていきます。

DTO をエンティティへ自動的にマッピングする

ModelMapper ライブラリには Spring のために特別に設計された拡張がありますが、これから実行することには役立たないので使用しません。これから DTO を処理する RESTful API を構築していき、これら DTO をできるだけ自動的にエンティティへ変換したいので、この魔法をするために独自のセットの汎用クラスを作成します。

用心深い読者は

ExamUpdateDTO
クラスの
id
プロパティに
@Id
のマークがついていることにお気づきだと思います。このソリューションはデーターベースに保存される既存エンティティのインスタンスをフェッチするために Spring MVC、JPA/Hibernate、および ModelMapper とこれら
@Ids
の値と統合するので、この注釈を追加しました。
@Id
プロパティを含まない DTO の場合、データベースを照会せずに、送信された値を基にした新しいエンティティを作ります。

Exam
のインスタンスとその DTO のみを処理するようにこのソリューションを制限できますが、QuestionMarks プロジェクトが拡大するにつれて、新しい DTO や新しいエンティティをお互いに変換しなければなりません。ですから、汎用ソリューションを作成して、発生するエンティティや DTO のシナリオを処理するのが状況にかないます。

これから作成する最初のアーティファクトは DTO をエンティティへ自動的にマッピングする注釈です。

com.questionmarks
パッケージに
util
と呼ばれる新しいパッケージを作り、次のコードでそれに
DTO
インターフェイスを作ります。

package com.questionmarks.util;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface DTO {
    Class value();
}

このインターフェイスは

@interface
と定義される注釈を実際に生成し、ランタイム (
@Retention (RetentionPolicy.RUNTIME)
) のメソッド パラメータ (
@Target (ElementType.PARAMETER)
) で使用されることを意図とします。この注釈が公開する唯一のプロパティは
value
で、その目標はどの DTO からエンティティが生成/更新されるかを定義することです。

次に作成する要素はその作業を担当するクラスです。このクラスは DTO の一部の構造に適合するユーザーによるリクエストを取得し、特定のエンティティ上の DTO を変換します。このクラスは送信した DTO が

@Id
を含む場合のために、データベースの照会も担当します。このクラスを
DTOModelMapper
と呼び、次のソースコードでそれを
com.questionmarks.util
パッケージ内に生成しましょう。

package com.questionmarks.util;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.modelmapper.ModelMapper;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;

import javax.persistence.EntityManager;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.Collections;

public class DTOModelMapper extends RequestResponseBodyMethodProcessor {
    private static final ModelMapper modelMapper = new ModelMapper();

    private EntityManager entityManager;

    public DTOModelMapper(ObjectMapper objectMapper, EntityManager entityManager) {
        super(Collections.singletonList(new MappingJackson2HttpMessageConverter(objectMapper)));
        this.entityManager = entityManager;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(DTO.class);
    }

    @Override
    protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
        binder.validate();
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        Object dto = super.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
        Object id = getEntityId(dto);
        if (id == null) {
            return modelMapper.map(dto, parameter.getParameterType());
        } else {
            Object persistedObject = entityManager.find(parameter.getParameterType(), id);
            modelMapper.map(dto, persistedObject);
            return persistedObject;
        }
    }

    @Override
    protected Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
        for (Annotation ann : parameter.getParameterAnnotations()) {
            DTO dtoType = AnnotationUtils.getAnnotation(ann, DTO.class);
            if (dtoType != null) {
                return super.readWithMessageConverters(inputMessage, parameter, dtoType.value());
            }
        }
        throw new RuntimeException();
    }

    private Object getEntityId(@NotNull Object dto) {
        for (Field field : dto.getClass().getDeclaredFields()) {
            if (field.getAnnotation(Id.class) != null) {
                try {
                    field.setAccessible(true);
                    return field.get(dto);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        return null;
    }
}

これは、ここまで生成したクラスの中で最も複雑ですから、良くご理解いただくために細かく説明していきましょう。

  1. このクラスは
    RequestResponseBodyMethodProcessor
    に拡張します。リクエストをクラスに変換するプロセス全体を書かなくてもいいように、このプロセッサを利用します。Spring MVC に慣れている方のために、拡張されたクラスは
    @RequestBody
    parameters
    を処理し事前設定するものです。これは、例えば JSON 本文などを取り、クラスのインスタンスで変換することを意味します。今回は、基本クラスを調整して、代わりに DTO のインスタンスを事前設定します。
  2. このクラスは
    ModelMapper
    のスタティック インスタンスを含みます。このインスタンスは DTO をエンティティへマップするために使用されます。
  3. このクラスは
    EntityManager
    のインスタンスを含みます。DTO を介してパスした
    id
    を基に、既存エンティティのデータベースをクエリできるように、このクラスにエンティティ マネージャを挿入します。
  4. supportsParameter
    メソッドを上書きします。このメソッドを上書きせずに、新しいクラスは丁度、基本クラスのように
    @RequestBody
    パラメータに適用されます。ですから
    @DTO
    注釈のみに適用されるように調整する必要があります。
  5. validateIfApplicable
    を上書きします。基本クラスはパラメータに
    @Valid
    または
    @Validated
    のマークが付いている場合のみ Bean Validation を実行します。この動作を変更してすべての DTO に bean validation を適用します。
  6. resolveArgument
    を上書きします。これはこの実装で最も重要なメソッドです。このプロセスでそれを調整して
    ModelMapper
    インスタンスを埋め込み、DTO をエンティティへマップします。しかし、マッピングする前に、新しいエンティティを処理するか、または既存エンティティへ DTO によって提案された変更を適用しなければならないかをチェックします。
  7. readWithMessageConverters
    メソッドを上書きします。基本クラスはこのパラメターのタイプを取り、このリクエストをそれのインスタンスに変換します。このメソッドを上書きしてこの変換が
    DTO
    注釈で定義されたタイプにし、DTO からエンティティへのマッピングを
    resolveArgument
    メソッドに残します。
  8. getEntityId
    メソッドを定義します。このメソッドは事前設定される DTO のフィールドで反復し、
    @Id
    のマークが付いているものをチェックします。それが見つかれば、フィールドの値が返され、
    resolveArgument
    はそれと共にデータベースをクエリできるようになります。

サイズは大きいですが、このクラスの実装を理解するのは難しくありません。要約すると、これは DTO のインスタンスを事前設定し、

@DTO
注釈で定義され、この DTO のプロパティをエンティティへマップします。これを魅力的にするには、エンティティの新しいインスタンスを常に事前設定する代わりに、まず、データベースから既存エンティティをフェッチする必要があるか否かを見るために DTO に
@Id
プロパティがあるかをチェックします。

この Spring Boot アプリケーションで

DTOModelMapper
クラスをアクティブ化するには、
WebMvcConfigurerAdapter
を拡張して引数リゾルバとしてそれを追加します。次のコンテンツで、
com.questionmarks
パッケージに
WebMvcConfig
と呼ばれるクラスを作りましょう。

package com.questionmarks;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.questionmarks.util.DTOModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import javax.persistence.EntityManager;
import java.util.List;

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    private final ApplicationContext applicationContext;
    private final EntityManager entityManager;

    @Autowired
    public WebMvcConfig(ApplicationContext applicationContext, EntityManager entityManager) {
        this.applicationContext = applicationContext;
        this.entityManager = entityManager;
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        super.addArgumentResolvers(argumentResolvers);
        ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().applicationContext(this.applicationContext).build();
        argumentResolvers.add(new DTOModelMapper(objectMapper, entityManager));
    }
}

WebMvcConfig
構成クラスのインスタンスが Spring によって作成されると、
ApplicationContext
および
EntityManager
の2つのコンポーネントが挿入されます。後者は
DTOModelMapper
を作るために使用され、前に説明したように、データベースをクエリするのに役立ちます。
ApplicationContext
ObjectMapper
のインスタンスを作るために使用されます。 このマッパーは Java オブジェクト間を変換したり JSON 構造を一致する機能を提供し、
DTOModelMapper
とそのスーパークラス
RequestResponseBodyMethodProcessor
によって必要とされます。

DTO をエンティティへ自動的に Spring Boot 上にマッピングする

これをツイートする

このプロジェクトの

WebMvcConfig
が正しく構成されたので、RESTful API 上の
@DTO
注釈を利用して自動的に DTO をエンティティへマップします。これを実行するために、試験を作成・更新するリクエストに同意するエンドポイントや、すべての既存の試験をリストにするエンドポイントを表示するコントローラーを作成していきます。このコントローラーを作成する前に、試験の永続化を処理できるようにするクラスを作っていきます。このクラスを
ExamRepository
と呼び、次のコードで
com.questionmarks.persistence
という新しいパッケージを作っていきます。

package com.questionmarks.persistence;

import com.questionmarks.model.Exam;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ExamRepository extends JpaRepository<Exam, Long> {
}

JpaRepository
インターフェイスには
save (Exam exam)
findAll ()
、および
delete (Exam exam)
のようなメソッドが含まれていますので、 ほかに実装する必要があるものはありません。よって、このレポジトリ インターフェイスを使用し、上記のエンドポイントを公開するコントローラーを作成できます。
com.questionmarks.controller
と呼ばれる新しいパッケージを作り、それに
ExamRestController
と呼ばれるクラスを追加しましょう。

package com.questionmarks.controller;

import com.questionmarks.model.Exam;
import com.questionmarks.model.dto.ExamCreationDTO;
import com.questionmarks.model.dto.ExamUpdateDTO;
import com.questionmarks.persistence.ExamRepository;
import com.questionmarks.util.DTO;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/exams")
public class ExamRestController {
    private ExamRepository examRepository;

    public ExamRestController(ExamRepository examRepository) {
        this.examRepository = examRepository;
    }

    @GetMapping
    public List<Exam> getExams() {
        return examRepository.findAll();
    }

    @PostMapping
    public void newExam(@DTO(ExamCreationDTO.class) Exam exam) {
        examRepository.save(exam);
    }

    @PutMapping
    @ResponseStatus(HttpStatus.OK)
    public void editExam(@DTO(ExamUpdateDTO.class) Exam exam) {
        examRepository.save(exam);
    }
}

このクラスの実装はとても簡単でした。各エンドポイントに1つのメソッドで3つのメソッドを作り、コンストラクターを介して

ExamRepository
インターフェイスを挿入しました。定義された最初のメソッド
getExams
GET
リクエストを処理し、例のリストを返すために実装されました。2つめのエンドポイント
newExam
ExamCreationDTO
を含む
POST
リクエストを処理し、
DTOModelMapper
の助けで
Exam
の新しいインスタンスに変換するために実装されました。3つめで最後のメソッドは
editExam
と呼ばれ、
PUT
リクエストを処理するエンドポイントとして定義され、
ExamUpdateDTO
オブジェクトを既存の
Exam
インスタンスに変換します。

この最後のメソッドが永続化

Exam
のインスタンスを見つける DTO を介して送信された
id
を使用し、メソッドを提供する前に3つのプロパティを置換することを強調することが重要です。置換されたプロパティは
title
description
、および
editedAt
で、
ExamUpdateDTO
で定義されたとおりです。

IDE を介してまたは

gradle bootRun
コマンドを介してここでアプリケーションを実行すると、このアプリケーションが起動し、ユーザーは生成したエンドポイントとの相互作用が可能になります。次のコマンドのリストは生成した DTO を使って、試験を生成、更新、取得するための
curl
の使い方を示します。

# retrieves all exams
curl http://localhost:8080/exams

# adds a new exam
curl -X POST -H "Content-Type: application/json" -d '{
    "title": "JavaScript",
    "description": "JS developers."
}' http://localhost:8080/exams

# adds another exam while ignoring fields not included in the DTO
curl -X POST -H "Content-Type: application/json" -d '{
    "title": "Python Interview Questions",
    "description": "An exam focused on helping Python developers.",
    "published": true
}' http://localhost:8080/exams

# updates the first exam changing its title and description
curl -X PUT -H "Content-Type: application/json" -d '{
    "id": 1,
    "title": "JavaScript Interview Questions",
    "description": "An exam focused on helping JS developers."
}' http://localhost:8080/exams

Aside: Securing Spring APIs with Auth0

Securing Spring Boot APIs with Auth0 is easy and brings a lot of great features to the table. With Auth0, we only have to write a few lines of code to get solid identity management solution, single sign-on, support for social identity providers (like Facebook, GitHub, Twitter, etc.), and support for enterprise identity providers (like Active Directory, LDAP, SAML, custom, etc.).

In the following sections, we are going to learn how to use Auth0 to secure APIs written with Spring Boot.

Creating the API

First, we need to create an API on our free Auth0 account. To do that, we have to go to the APIs section of the management dashboard and click on "Create API". On the dialog that appears, we can name our API as "Contacts API" (the name isn't really important) and identify it as

https://contacts.blog-samples.com
(we will use this value later).

Registering the Auth0 Dependency

The second step is to import a dependency called

auth0-spring-security-api
. This can be done on a Maven project by including the following configuration to
pom.xml
(it's not harder to do this on Gradle, Ivy, and so on):

<project ...>
    <!-- everything else ... -->
    <dependencies>
        <!-- other dependencies ... -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>auth0-spring-security-api</artifactId>
            <version>1.0.0-rc.3</version>
        </dependency>
    </dependencies>
</project>

Integrating Auth0 with Spring Security

The third step consists of extending the WebSecurityConfigurerAdapter class. In this extension, we use

JwtWebSecurityConfigurer
to integrate Auth0 and Spring Security:

package com.auth0.samples.secure;

import com.auth0.spring.security.api.JwtWebSecurityConfigurer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Value(value = "${auth0.apiAudience}")
    private String apiAudience;
    @Value(value = "${auth0.issuer}")
    private String issuer;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        JwtWebSecurityConfigurer
                .forRS256(apiAudience, issuer)
                .configure(http)
                .cors().and().csrf().disable().authorizeRequests()
                .anyRequest().permitAll();
    }
}

As we don't want to hard code credentials in the code, we make

SecurityConfig
depend on two environment properties:

  • auth0.apiAudience
    : This is the value that we set as the identifier of the API that we created at Auth0 (
    https://contacts.blog-samples.com
    ).
  • auth0.issuer
    : This is our domain at Auth0, including the HTTP protocol. For example:
    https://blog-samples.auth0.com/
    .

Let's set them in a properties file on our Spring application (e.g.

application.properties
):

auth0.issuer:https://blog-samples.auth0.com/
auth0.apiAudience:https://contacts.blog-samples.com/

Securing Endpoints with Auth0

After integrating Auth0 and Spring Security, we can easily secure our endpoints with Spring Security annotations:

package com.auth0.samples.secure;

import com.google.common.collect.Lists;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping(value = "/contacts/")
public class ContactController {
    private static final List<Contact> contacts = Lists.newArrayList(
            Contact.builder().name("Bruno Krebs").phone("+5551987654321").build(),
            Contact.builder().name("John Doe").phone("+5551888884444").build()
    );

    @GetMapping
    public List<Contact> getContacts() {
        return contacts;
    }

    @PostMapping
    public void addContact(@RequestBody Contact contact) {
        contacts.add(contact);
    }
}

Now, to be able to interact with our endpoints, we will have to obtain an access token from Auth0. There are multiple ways to do this and the strategy that we will use depends on the type of the client application we are developing. For example, if we are developing a Single Page Application (SPA), we will use what is called the Implicit Grant. If we are developing a mobile application, we will use the Authorization Code Grant Flow with PKCE. There are other flows available at Auth0. However, for a simple test like this one, we can use our Auth0 dashboard to get one.

Therefore, we can head back to the APIs section in our Auth0 dashboard, click on the API we created before, and then click on the Test section of this API. There, we will find a button called Copy Token. Let's click on this button to copy an access token to our clipboard.

Copying a test token from the Auth0 dashboard.

After copying this token, we can open a terminal and issue the following commands:

# create a variable with our token
ACCESS_TOKEN=<OUR_ACCESS_TOKEN>

# use this variable to fetch contacts
curl -H 'Authorization: Bearer '$ACCESS_TOKEN http://localhost:8080/contacts/

Note: We will have to replace

<OUR_ACCESS_TOKEN>
with the token we copied from our dashboard.

As we are now using our access token on the requests we are sending to our API, we will manage to get the list of contacts again.

That's how we secure our Node.js backend API. Easy, right?

次のステップ:例外処理および I18N

@DTO
注釈とそのコンパニオン
DTOModelMapper
を使ってエンティティの実装詳細を簡単に非表示にできるしっかりした基本を構築しました。合わせて、DTO をエンティティへ自動的にマッピングし、これら DTO を介して送信されたデータを検証することで RESTful エンドポイントの開発プロセスをスムーズにします。ここで欠落しているのは、これらの検証期間にスローされた例外や、フライト期間に起きるかもしれない予期されない例外を処理する適切な方法です。

私たちは API を購入する誰もにできるだけ素晴らしい経験を提供したいと願っています。これにはよくフォーマットされたエラー メッセージを与えることも含まれます。それ以上に、私たちは英語以外の他の言語を話すユーザーとコミュニケーションができるようにしたいと思います。よって、次回のアーティクルでは、Spring Boot API で例外の処理や I18N(国際化)に取り組んでいきます。お見逃しなく!