Spring Boot + ReactでWebシステムを開発する方法

Spring Boot + React(TypeScript)で簡単なWebシステムを開発してAWSにデプロイまでしたので、その方法を紹介します。

注意点
  • 間違っている可能性もあるので自己責任で参考にしてください。
  • ブログを書く時間がないため、現時点ではバックエンドのみ書いています。
目次

開発するシステム概要

今回は、簡易的なユーザー情報の管理システムを開発します。システムの概要は以下の通りです。

バックエンド

プログラミング言語はJava、フレームワークはSpring Bootを使ってAPIを構築します。データベースは何でも良いですが、無料で提供されているPostgreSQLを使います。

データベースへの接続はMyBatis(マイバティス)というフレームワークを使います。MyBatisを経由してPostgreSQLにSQLの発行を行い、データの取得や更新を行います。

フロントエンド

フロントエンドは、JavaScriptのライブラリであるReactを使います。また、型付けを行うためにTypeScriptを導入します。

そして、上記で構築したバックエンドのAPIにHTTPのリクエストを行い、データを取得して画面に表示したり、画面で入力された情報をデータベースに登録したりします。

デプロイ

システム開発が完了したら、AWSのクラウド環境にデプロイして公開を行います。デプロイする方法はいくつかありますが、簡単にデプロイできるElastic Beanstalkを使用します。

開発環境

バックエンドの開発はEclipse(厳密にはPleiades All in One)、フロントエンドはVisual Studio Codeを使用します。

バックエンドの開発

プロジェクトの作成

Eclipseで「ファイル」⇒「新規」⇒「Springスターター・プロジェクト」をクリックします。

名前の部分に任意の名前を入力します。タイプの項目で「Gradle」か「Maven」のどちらかをビルドツールとして選択できますが、今回はGradleを使います。

依存関係のページでは、以下を導入します。必要に応じてその他も自由に入れてください。

依存関係
  • Lombok(コードの自動生成を行うツールで開発速度が上がる)
  • Spring Boot DevTools(ソースコードを修正したときに自動で開発サーバに反映させる)
  • Spring Web(MVCアーキテクチャの手法で開発ができる)
  • Thymeleaf(Spring Bootのテンプレートエンジン。なくても良いが、一応入れておく)
  • Validation(入力検証を行う)
  • Mybatis Framework(MyBatisが使える)
  • PostgreSQL Driver(PostgreSQLが使える)

プロジェクトを作成したら、build.gradleというファイルを開いてみてください。dependenciesという部分に先ほど選択した依存関係が記述されているはずです。

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'org.postgresql:postgresql'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

implementationやruntimeOnlyなどは、依存関係のライブラリをどこに含めるかを指定します。例えばimplementationを選ぶと、コンパイル時およびビルドされたjarファイルに依存関係のライブラリが含まれます。runtimeOnlyを選ぶと、コンパイル時は含まれないが、jarファイルには含まれます。

もしもプロジェクトを作成した後にライブラリを追加したくなったら、上記のようにbuild.gradleのdependenciesに追記を行います。そして、build.gradleを右クリックして「Gradle」⇒「Gradleプロジェクトのリフレッシュ」を実行します。すると、「プロジェクトと外部の依存関係」に追加されると思います。

データベースの接続情報を設定

PostgreSQLのデータベースやテーブルなどを作成したら、接続情報を構成ファイルに設定します。

Spring Bootにはapplication.propertiesという構成ファイルがデフォルトで用意されています。application.propertiesは「.yml(ヤムル)」という拡張子に変更することもできて、この記事ではYAML形式でサンプルを紹介しています。

補足

YAML形式に変更するには、application.propertiesを右クリックして「リファクタリング」⇒「名前変更」で「application.yml」を入力します。

以下はデータベースの接続情報を設定するサンプルです。DBの名前やユーザ名、パスワードなどは変更してください。

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/DBの名前
    username: ユーザ名
    password: パスワード

モデルの作成

データベースから取得した情報や、画面で入力された情報などを格納するモデル(MVCのModel)を作成します。

modelというフォルダを作成して、その中に以下のようなクラスを作成します。

@Getter
@Setter
public class Person {

	private int id;

	private String name;

	private int age;
}

今回はユーザー情報の管理システムなので、Personというクラスにしています。データベースのカラムに合わせて、ID、名前、年齢のフィールドを用意しています。

Lombokを依存関係に追加しているため、@Getterと@Setterというアノテーションをつけると、GetterとSetterを自動で作成してくれます。そのため、getName()やsetAge(30)といった感じで値の取得や設定が可能です。

Mapperインタフェースの作成

mapperというフォルダを作成して、その中にMapperインタフェースを作ります。Mapperインタフェースには、以下のようにデータベースの取得や更新を行う際のメソッド名や引数、戻り値などを定義します。

@Mapper
public interface PersonMapper {

	Person getPerson(int id);

	void updatePerson(Person person);
}

例えば、getPersonというメソッドは、int型のidを条件にユーザー情報の取得を行い、先ほど作成したPersonクラスのオブジェクトが戻り値になります。

また、updatePersonというメソッドは、画面で入力された情報を格納したPersonクラスのオブジェクトをもとにデータベースの更新を行い、戻り値はありません。

@Mapperというアノテーションをつけることで、MapperインタフェースとしてSpringフレームワークから認識されます。

上記のサンプルでは引数が一つですが、もしも複数になる場合は@Paramを使う必要があります。

Person getPerson(@Param("id") int id, @Param("name") String name);

Mapper XMLの作成

次に、Mapperのインタフェースに対応したXMLファイルを作成して、SQLを記述します。

注意点としては、MapperのXMLは「src/main/resources」配下で、かつインタフェースと同じ階層に作る必要があります。例えば、以下のようなイメージです。

(Mapperのインタフェース)
src/main/java/com/example/demo/mapper/PersonMapper.java

(MapperのXML)
src/main/resources/com/example/demo/mapper/PersonMapper.xml

上記のようにすることで、フレームワークがXMLファイルの場所を見つけられます。

PersonMapper.javaとPersonMapper.xmlを同一階層ではなく同一フォルダに配置していたところ、ローカル環境では上手くいきましたが、AWS環境では以下のエラーが出ました。

エラー

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)

MapperのXMLには、以下のようにSQLを記述します。実際の例を見るとイメージがわきやすいと思います。

<?xml version="1.0" encoding ="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.demo.mapper.PersonMapper">
	<select id="getPerson" resultType="com.example.demo.model.Person">
		select
			id,
			name,
			age
		from person
		where id = #{id}
	</select>
	<update id="updatePerson">
		update
			person
		set
			name = #{name},
			age = #{age}
		where
			id = #{id}
	</update>
</mapper>

まず、mapperタグのnamespace属性に、Mapperインタフェースの名前空間を記述します。これによって、インタフェースとXMLが紐づきます。

SELECTやUPDATEなどSQLの構文ごとに<select>や<update>といったタグが用意されています。idにはMapperインタフェースのメソッド名を記述することで、メソッドとSQLが紐づきます。

SELECTの場合は、取得したデータの格納先をresultTypeに指定します。上記の例ではPersonクラスを指定しているので、Personテーブルのid、name、ageが、それぞれPersonクラスのid、name、ageに設定されます。

注意点

データベースのカラム名とresultTypeのフィールド名が異なる場合、そのままではマッピングできません。例えば、Personテーブルのカラム名が「user_name」で、Personクラスのフィールド名が「name」の場合です。

このような場合は、以下のように「as」を使うことでマッピングできます。

select
user_name as name
from person

※他にも、application.propertiesやresultMapを使う方法がありますが、ここでは割愛します。

Mapperインタフェースの引数に設定した情報は、「#{}」という形式で取り出せます。

補足

「#{}」という形式で記述した場合、PreparedStatementになってSQLインジェクション対策ができます。もしも勝手にエスケープされては困る場合は、「${}」という形式を使用します。

サービスの作成

データベースへのアクセスや業務ロジックなどを記述するサービスクラスを作成します。

serviceというフォルダを作成して、その中に以下のようなクラスを作成します。

@Service
@Transactional
public class PersonService {

	@Autowired
	private PersonMapper mapper;

	public Person getPerson(int id) {
		return mapper.getPerson(id);
	}

	public void updatePerson(Person person) {
		mapper.updatePerson(person);
	}
}

サービスクラスには@Serviceというアノテーションをつけます。これによってサービスであることを明示できると共に、DIコンテナに登録されます。

DIについて理解するには、上記のコードのmapperを見ると分かりやすいです。通常、あるクラスのメソッドを利用するには、以下のようにインスタンスを生成します。

public Person getPerson(int id) {
    PersonMapperImpl mapper = new PersonMapperImpl();
    return mapper.getPerson(id);
}

まずは、Mapperインタフェースを実装したクラス(PersonMapperImpl)を作成します。そして、そのクラスのオブジェクトをインスタンス化してメソッドを呼び出します。

しかし、今回は実装クラスを作成していませんし、インスタンス化もしていません。

実装クラスに関しては、MyBatisが自動で生成してくれます。インスタンス化に関しては、mapperというフィールドに@Autowiredというアノテーションをつけることで、フレームワークのDIコンテナがインスタンスを生成してフィールドに設定してくれます。

@Autowiredをつけるだけでは不十分で、DIの対象となるクラスには、@Service、@Controller、@Componentといったアノテーションを付与する必要があります。

DIの説明が終わったところで、メソッドの中身を見てみます。やっていることは非常にシンプルで、Mapperインタフェースのメソッドを呼び出します。これによって、対応するMapperのXMLに記述したSQLが実行され、その結果が戻り値として返ってきます。

@Transactionalのアノテーションに関しては、Spring BootのAOPという仕組みを使って、メソッド全体をトランザクションとして処理してくれます。そのため、メソッドを抜けるまでにエラーがなければコミットを行い、エラーがあればロールバックしてくれます。

このように、SpringフレームワークはDIや各種アノテーションを利用することで、コードを書く量を減らせることがメリットの一つです。

コントローラの作成

APIとして画面からのリクエストを受け付け、サービスの結果などをレスポンスとして返すコントローラを作成します。

controllerというフォルダを作成して、その中に以下のようなクラスを作成します。

@RestController
@RequestMapping("api")
public class PersonController {

	@Autowired
	private PersonService service;

	@GetMapping("/person/{id}")
	public Person getPerson(@PathVariable("id") String id) {
		return service.getPerson(Integer.parseInt(id));
	}
}

通常のコントローラはビューと呼ばれる画面を返しますが、今回は画面部分をReactで別途作成するため、APIはデータの取得や更新のみを担当します。

ビューではなくデータ自体をレスポンスのbodyに設定して返すには、メソッドに@ResponseBodyというアノテーションをつけます。ただし、今回のようにコントローラ全体がAPIのような場合には、コントローラに@RestControllerというアノテーションをつけることで対応できます。

@RequestMappingというアノテーションをつけて、コントローラに紐づけたいURLを指定します。今回はAPIに対する全てのURLに「api」という文字を含めています。

全てのリクエストに「api」をつける場合、application.ymlに設定することもできますが、今回は後述するAPI以外のコントローラもあるので使いません。

server:
  servlet:
    context-path: /api

@GetMappingというアノテーションは、HTTPのGETメソッドを受け付けます。アノテーションにはそのメソッドに紐づけたいURLの形式を指定できます。上記の例では「xxxx.com/api/person/1」や「xxxx.com/api/person/2」といったURLが該当します。

メソッドの中身を簡単に説明すると、getPersonはidを受け取ってサービスに渡し、データベースで検索したPersonクラスのオブジェクトを返しています。URLに含まれる値は「@PathVariable("名前")」で取得できます。

本来はidの検証なども必要ですが、ここでは分かりやすいように省略しています。

次に、ユーザー情報を更新するメソッドを作成します。

@PutMapping("/person/update")
public ApiError updatePerson(@Validated @RequestBody Person person, BindingResult result) {

	if (result.hasErrors()) {
		Map<String, String> errors = new HashMap<>();
		for (FieldError error : result.getFieldErrors()) {
			errors.put(error.getField(), error.getDefaultMessage());
		}

		return new ApiError(errors);
	}

	service.updatePerson(person);

	return new ApiError(null);
}

更新処理は一般的にHTTPのPUTメソッドを使います。そこで、@PutMappingというアノテーションをつけて、マッピングさせたいURLを記述します。@RequestBodyを引数につけることで、リクエストで送られたユーザー情報が自動で設定されます。

入力検証を自動で行いたい場合、@Validatedというアノテーションを付与します。入力エラーの有無はBindingResultのhasErrorsメソッドで判定できるので、エラーがあったときにはApiErrorという自作のオブジェクトにエラー情報を設定して返します。

Spring Bootで入力検証を行うには、以下の3点が必要です。

入力検証の準備
  1. spring-boot-starter-validationを依存関係に追加
  2. 入力検証の内容をフィールドのアノテーションに追加
  3. メソッドの引数に@Validatedアノテーションを付与する

依存関係の追加と@Validatedアノテーションの付与は既に実施しているので、二つ目の作業を行います。以下のように、検証対象のPersonクラスに対して、専用のアノテーションをつけます。

private int id;

@NotBlank(message = "名前を入力してください。")
@Length(max = 20, message = "名前は20文字以内で入力してください。")
private String name;

@Range(min = 1, max = 200, message = "年齢は1~200までの半角数字を入力してください。")
private int age;

例えば、@NotBlankをつければ必須チェックになり、@Lengthをつければ文字数のチェックができます。他にもアノテーションはあるので、調べてみてください。

エラーメッセージをカスタマイズしたい場合は、上記のサンプルのようにmessageに設定します。

ビューの作成

今回のシステムは、Reactを使った「SPA(シングルページアプリケーション)」です。ユーザーには単一のHTMLを返して、その後はJavaScriptを用いて画面の内容が部分的に更新されます。

そのため、以下のようなHTMLファイルを「src/main/resources/templates」の配下に作成します。

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, viewport-fit=cover">
	<title>XXXXX</title>
	<link rel="stylesheet" href="/static/style/style.css">
	<link rel="shortcut icon" href="/static/img/favicon.ico">
	<link rel="apple-touch-icon" href="/static/img/apple-touch-icon.png">
</head>
<body>
	<noscript>JavaScriptを有効化してください。</noscript>
	<div id="root"></div>
	<script src="/static/built/bundle.js"></script>
</body>
</html>

タグは一部省略しているので、必要に応じて追加・変更してください。スタイルシートや画像などの静的ファイルは「src/main/resources/templates」直下の「static」フォルダに配置することを想定しています。

ポイントは、bundle.jsの部分です。ReactおよびTypeScriptで開発したソースをbundle.jsにビルドして、それを読み込んでいます。

補足

エラーが発生したときのために、index.htmlの他にerror.htmlも用意しておくと良いと思います。

SPA用のコントローラの作成

画面の遷移はReact Routerを使うことになりますが、初回のアクセスでは先ほど作成したindex.htmlを返す必要があります。

トップページはもちろんのこと、どこか途中のページからアクセスやリロードされることもありえるため、全てのリクエストでindex.htmlを返すようにします。

そこで、以下のようなコントローラを作成します。

@Controller
@RequestMapping
public class HomeController {

	@GetMapping("{path:^(?!.*static).*$}/**")
	public String all() {
		return "index";
	}
}

allというメソッドは「index」という文字列を返しています。ここで指定した文字列はビューの名称を表しており、Springフレームワークが「src/main/resources/templates」配下から対応するHTMLを返します。

このようにビューを返す場合は、@RestControllerではなく@Controllerというアノテーションをつけます。

基本的には全てのURLに対してindex.htmlを返すのですが、スタイルシートや画像などの静的ファイルは除外しないとそれらが取得できなくなります。そこで、@GetMappingに指定するURLには、正規表現を使って「static/」から始まるURLを除外しています。

通常staticフォルダの配下に置いたファイルは、以下のように「static」をつけないURLでアクセスできます。

(静的ファイルの配置場所)
src/main/resources/static/img/favicon.ico

(静的ファイルのURL)
sample.com/img/favicon.ico

それでは先ほどの正規表現が機能しないので、application.ymlを修正して静的ファイルのURLに「static」がつくようにします。

spring:
  mvc:
    static-path-pattern: /static/**
目次
閉じる