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;
}
}
これは、ここまで生成したクラスの中で最も複雑ですから、良くご理解いただくために細かく説明していきましょう。
- このクラスは
RequestResponseBodyMethodProcessor
に拡張します。リクエストをクラスに変換するプロセス全体を書かなくてもいいように、このプロセッサを利用します。Spring MVC に慣れている方のために、拡張されたクラスは@RequestBody
parameters を処理し事前設定するものです。これは、例えば JSON 本文などを取り、クラスのインスタンスで変換することを意味します。今回は、基本クラスを調整して、代わりに DTO のインスタンスを事前設定します。 - このクラスは
ModelMapper
のスタティック インスタンスを含みます。このインスタンスは DTO をエンティティへマップするために使用されます。 - このクラスは
EntityManager
のインスタンスを含みます。DTO を介してパスしたid
を基に、既存エンティティのデータベースをクエリできるように、このクラスにエンティティ マネージャを挿入します。 supportsParameter
メソッドを上書きします。このメソッドを上書きせずに、新しいクラスは丁度、基本クラスのように@RequestBody
パラメータに適用されます。ですから@DTO
注釈のみに適用されるように調整する必要があります。validateIfApplicable
を上書きします。基本クラスはパラメータに@Valid
または@Validated
のマークが付いている場合のみ Bean Validation を実行します。この動作を変更してすべての DTO に bean validation を適用します。resolveArgument
を上書きします。これはこの実装で最も重要なメソッドです。このプロセスでそれを調整してModelMapper
インスタンスを埋め込み、DTO をエンティティへマップします。しかし、マッピングする前に、新しいエンティティを処理するか、または既存エンティティへ DTO によって提案された変更を適用しなければならないかをチェックします。readWithMessageConverters
メソッドを上書きします。基本クラスはこのパラメターのタイプを取り、このリクエストをそれのインスタンスに変換します。このメソッドを上書きしてこの変換がDTO
注釈で定義されたタイプにし、DTO からエンティティへのマッピングをresolveArgument
メソッドに残します。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.
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(国際化)に取り組んでいきます。お見逃しなく!