2019.04.02

Spring BootでWebアプリケーション開発 (2) データの取得と返却


この記事ではSpring Bootを使って、以下のような簡単な辞書アプリを作成します。

完成図

前回は以下を行いました。

  • Eclipseによる環境構築
  • O/Rマッパーを利用するための、ドメインクラスの実装
  • O/Rマッピング用のインターフェースとXMLファイルの作成

今回は、辞書アプリの初期画面を表示するためのデータ取得処理を実装します。

サービス

まずは前回作成したマッパーを呼び出すためのサービスクラスを作成します。

マッパークラスは後述するコントローラークラスから呼び出すことは推奨されていません。コントローラークラスはあくまでHTTPリクエストやレスポンスを管理するためのクラスで、ビジネスロジックを書くためのクラスではないためです。

そのため、ビジネスロジックを書くためのサービスクラスに、マッパーを呼び出すための記述を行います。

インターフェース

src/main/java 配下に com.example.dictionary.service パッケージを用意して、WordService.javaFieldService.java を作成します。

これらのファイルはサービスクラスのインターフェースとなります。

WordService.java
package com.example.dictionary.service;

import java.util.List;

import com.example.dictionary.domain.Word;

/*
 * 単語に関するサービスを提供するクラス
 */
public interface WordService {

    /**
     * すべての単語を取得する
     *
     * @return 単語リスト
     */
    public List<Word> searchAllWord();

    /**
     * 分野IDを基に単語リストを取得する
     *
     * @param 単語Id
     * @return 単語リスト
     */
    public List<Word> searchByFieldId(int Id);
}
FieldService.java
package com.example.dictionary.service;

import java.util.List;

import com.example.dictionary.domain.Field;

/*
 * 分野に関するサービスを提供するクラス
 */
public interface FieldService {

    /**
     * すべての分野を取得する
     *
     * @return 分野リスト
     */
    public List<Field> searchAllField();
}

これらのインターフェースには、各テーブルの全レコードを取得するためのメソッドを定義します。

実装クラス

次に、先ほど用意した com.example.dictionary.service パッケージの配下に impl パッケージを用意します。そしてその中に、実装クラスである WordServiceImpl.javaFieldServiceImpl.java を作成します。

WordServiceImple.java
package com.example.dictionary.service.impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.dictionary.domain.Word;
import com.example.dictionary.mappers.WordMapper;
import com.example.dictionary.service.WordService;

@Service
public class WordServiceImpl implements WordService {

    @Autowired
    private WordMapper wordMapper;

    @Override
    public List<Word> searchAllWord() {
        List<Word> wordList = wordMapper.findAll();
        return wordList;
    }

    @Override
    public List<Word> searchByFieldId(int id) {
        List<Word> wordList = wordMapper.findByFieldId(id);
        return wordList;
    }

}
FieldServiceImple.java
package com.example.dictionary.service.impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.dictionary.domain.Field;
import com.example.dictionary.mappers.FieldMapper;
import com.example.dictionary.service.FieldService;

@Service
public class FieldServiceImpl implements FieldService {

    @Autowired
    private FieldMapper fieldMapper;

    @Override
    public List<Field> searchAllField() {
        List<Field> fieldList = fieldMapper.findAll();
        return fieldList;
    }

}

これらの実装クラス内には、マッパークラスを呼び出し、各テーブルのレコードを取得する処理が書かれています。

@Service のアノテーションを付与し忘れると、Autowired でインスタンス化することが出来なくなるので気をつけてください。

Autowiredとは

先ほど作成した実装クラスでは、マッパークラスを定義する記述の上に @Autowired というアノテーションがついています。このAutowiredはインターフェースとして宣言された変数に、特定のインスタンスを付与するための記述です。

付与される実装クラスには @Component@Service などのアノテーションがついている必要があります。これらのアノテーションがついている実装クラスがAutowiredの対象になるので、実装時はアノテーションの付け忘れに気をつけましょう。

Springを使ってWebアプリケーションを作成する際は、このAutowiredをフル活用することになるので気になる人は「Autowired DI」などで検索してみてください。

レスポンスフォーム

レスポンスフォームクラスは、レスポンスメッセージ作成に使う、ビジネスロジックの処理結果を格納します。レスポンスメッセージの生成処理は、後述のレスポンスリザルトクラスの役割です。

src/main/java 配下に com.example.dictionary.container.responseform パッケージを用意して、WebResponseForm.java というインターフェースを作成します。これが各リクエストに対するレスポンスフォームの共通インターフェースとなります。

次は、同じパッケージ内に DictionaryResponseForm.java を作成します。このクラスはレスポンスフォームなので、WebResponseForm インターフェースを実装する必要があります。

DictionaryResponseForm.java
package com.example.dictionary.container.responseform;

import java.util.ArrayList;
import java.util.List;

import com.example.dictionary.domain.Field;
import com.example.dictionary.domain.Word;

/*
 * 辞書用レスポンスフォーム
 */
public class DictionaryResponseForm implements WebResponseForm {

    /** 単語リスト */
    private List<Word> wordList = new ArrayList<>();

    /** 分野リスト */
    private List<Field> fieldList = new ArrayList<>();

    public List<Word> getWordList() {
        return wordList;
    }

    public void setWordList(List<Word> wordList) {
        this.wordList = wordList;
    }

    public List<Field> getFieldList() {
        return fieldList;
    }

    public void setFieldList(List<Field> fieldList) {
        this.fieldList = fieldList;
    }

}

このレスポンスフォームは、単語テーブルから取得したレコードと分野テーブルから取得したレコードを管理します。

レスポンスリザルト

次に、レスポンスリザルトクラスを作成します。レスポンスリザルトクラスは、レスポンスフォームクラスを受け取って、HTTPレスポンスメッセージを形成します。

src/main/java 配下の com.example.dictionary.container パッケージにWebResponseResult.java を作成します。

WebResponseResult.java
package com.example.dictionary.container;

import java.util.ArrayList;
import java.util.List;

import com.example.dictionary.container.responseform.WebResponseForm;

/*
 * 処理結果クラス
 */
public class WebResponseResult<T extends WebResponseForm> {

    /** レスポンスフォーム */
    private T responseForm;

    /** インフォメッセージリスト */
    List<String> infoMessageList = new ArrayList<>();

    /** エラーメッセージリスト */
    List<String> errorMessageList = new ArrayList<>();

    public T getResponseForm() {
        return responseForm;
    }

    public void setResponseForm(T responseForm) {
        this.responseForm = responseForm;
    }

    public List<String> getInfoMessageList() {
        return infoMessageList;
    }

    public void setInfoMessageList(List<String> infoMessageList) {
        this.infoMessageList = infoMessageList;
    }

    public List<String> getErrorMessageList() {
        return errorMessageList;
    }

    public void setErrorMessageList(List<String> errorMessageList) {
        this.errorMessageList = errorMessageList;
    }
}

レスポンスリザルトクラスは、以下のフィールドを保持しています。

  1. レスポンスフォーム
  2. 処理成功時のメッセージのリスト
  3. エラーメッセージのリスト

レスポンスフォームを格納する必要があるため、ジェネリクスで WebResponseForm を指定します。

また、2 と 3 のメッセージは必要に応じて設定します。

レシーバー

次に、レシーバークラスを作成します。レシーバークラスの処理は後述のコントローラークラスに書くこともできますが、それらをレシーバークラスとして抽出することで、後々のメンテナンスがしやすくなります。

また、コントローラークラスでリフレクションを用いてレシーバーとメソッドを呼び出す実装にするので、レシーバークラスではクライアントのやり取りを意識せずに実装できます。

src/main/java 配下に com.example.dictionary.contraller.webreceiver パッケージを作成し、DictionaryReceiver.java を作成します。

DictionaryReceiver.java
package com.example.dictionary.controller.WebReceiver;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.example.dictionary.container.WebResponceResult;
import com.example.dictionary.container.requestform.DictionaryRequestForm;
import com.example.dictionary.container.responseform.DictionaryResponseForm;
import com.example.dictionary.service.FieldService;
import com.example.dictionary.service.WordService;

@Component
public class DictionaryReceiver implements WebReceiver<DictionaryResponseForm> {

    @Autowired
    WordService wordService;

    @Autowired
    FieldService fieldService;

    @Override
    public WebResponceResult<DictionaryResponseForm> init() {
        WebResponceResult<DictionaryResponseForm> result = new WebResponceResult<>();

        DictionaryResponseForm responseForm = new DictionaryResponseForm();
        responseForm.setWordList(wordService.searchAllWord());
        responseForm.setFieldList(fieldService.searchAllField());

        result.setResponseForm(responseForm);

        return result;
    }
}

レシーバークラスは、コントローラークラスでAutowiredによって呼び出されるため @Component アノテーションを付与します。

また、DBにアクセスするためにマッパーを呼び出すことの出来る WordServiceFieldService をAutowiredによってインスタンス化します。

init メソッドでは単語テーブルの全レコードと分野テーブルの全レコードを取得して、レスポンスフォームに格納し、さらにレスポンスリザルトに格納して返す処理が記述されています。

コントローラー

最後に、コントローラークラスを作成します。

コントローラークラスは、クライアント側から送られてきたリクエストを各レシーバーに振り分け、処理結果を再度クライアントにレスポンスする処理を行います。

このクラスはクライアント側とビジネスロジックの仲介役となります。

src/main/java 配下の com.example.dictionary.controller パッケージにApplicationController.java を作成します。

ApplicationController.java
package com.example.dictionary.controller;

import java.lang.reflect.Method;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.example.dictionary.container.WebResponceResult;
import com.example.dictionary.container.requestform.AddWordRequestForm;
import com.example.dictionary.container.requestform.DictionaryRequestForm;
import com.example.dictionary.controller.WebReceiver.WebReceiver;

@RestController
public class ApplecationController {

    @Autowired
    private Map<String, WebReceiver<?>> receiverMap;

    @GetMapping("/{className}")
    public ResponseEntity<WebResponceResult<?>> index(@PathVariable String className) throws Exception {

        WebReceiver<?> receiver = receiverMap.get(className + "Receiver");

        //initメソッドを起動する
        Method method = receiver.getClass().getMethod("init");
        WebResponceResult<?> result = (WebResponceResult<?>) method.invoke(receiver);

        return new ResponseEntity<>(result, HttpStatus.OK);
    }
}

今回はサーバサイドプログラムを、クライアント側から独立したWeb APIとして実装するので、@RestController アノテーションを付与します。これにより、コントローラーメソッドの返却値が自動的にJSONに変換されてから、HTTPレスポンスとして送信されます。

また今回は、URLによって呼び出すレシーバーを切り分ける実装しています。

つまり、/directory というURLでアクセスされたときに DirectoryReceiver クラスの init メソッドを自動的に呼び出す仕組みです。

この仕組みは、以下の実装によって実現されています。

  1. レシーバクラスをMap型でAutowiredすることで、対象のインターフェースに代入することが出来る実装クラスをすべて、クラス名をキーとしてMap型に格納してくれます。
  2. GETリクエストを送られたときに、@GetMapping アノテーションを付与したメソッド index が実行されます。
  3. URLの {} で囲まれた変数部分は、@PathVariable を付与した引数として受け取ることが出来ます。今回は className という引数に、URLのパス部分が格納されます。
  4. レシーバクラスのマップから、className に入った文字列、つまりリクエストされたURLパスをキーに持つ実装クラスを取得します。
  5. 取得したレシーバクラスに対して、init メソッドを呼び出します。
  6. init メソッドからの返却値をそのままレスポンスして、一連の処理が完了します。

実行!!

ここまでで一度実行してみます。

パッケージ・エクスプローラーのdictionaryプロジェクトを右クリックして実行 → Spring Bootアプリケーションを選択しすると実行することが出来ます。

アプリ実行時のコンソール画面

上の画像のように、一番下の行に「Started DictionaryApplication」と表示されていれば実行成功です。この段階で実行が失敗した場合はおそらく設定ファイルかマイグレーション用のSQLファイルが間違っていますので見直してみてください。

うまくいった場合はブラウザを開いてhttp://localhost:8080/dictionaryにアクセスしてみてください。

{"responseForm":{"wordList":[],"fieldList":[]},"infoMessageList":[],"errorMessageList":[]}

上記のように書かれた画面が表示されていれば成功です。ここで失敗した場合はレシーバー、サービス、マッパーのどれかの処理が間違っている可能性があるので、見直してみてください。

表示された画面には、レスポンスリザルト内に格納されたレコードをJSONに変換したものが書かれています。[] の中に何もかかれていないのは、まだDBのテーブルに何も登録されていないためです。

しっかりレコードが取得できているかを確認するために、DBに初期データを登録してみましょう。

migration フォルダ内に V2__insert_initialize_data.sql を作成します。

V2__insert_initialize_data.sql
insert into word(word, field_id, word_desc) values('Spring Boot', 1, 'JavaでWebアプリを作成するためのフレームワーク');
insert into word(word, field_id, word_desc) values('オブジェクト指向', 1, 'Javaでプログラミングをする際の考え方');
insert into word(word, field_id, word_desc) values('Vue.js', 2, 'JavaScriptのライブラリ');
insert into field(field_name, field_genre) values('Java', 'IT');
insert into field(field_name, field_genre) values('JavaScript', 'IT');

SQLを作成した後に、アプリを再起動してみてください。

{"responseForm":{"wordList":[{"wordId":1,"word":"Sptirn boot","fieldId":1,"wordDesc":"JavaでWebアプリを作成するためのフレームワーク"},{"wordId":2,"word":"オブジェクト指向","fieldId":1,"wordDesc":"Javaでプログラミングをする際の考え方"},{"wordId":3,"word":"Vue.js","fieldId":2,"wordDesc":"JavaScriptのライブラリ"}],"fieldList":[{"fieldId":1,"fieldName":"Java","fieldGenre":"IT"},{"fieldId":2,"fieldName":"JavaScript","fieldGenre":"IT"}]},"infoMessageList":[],"errorMessageList":[]}

上記のように表示されていれば成功です。DBからデータが取得できていることが分かります。

次回予告

ここまででサーバー側の一連の処理を紹介しました。

今回やったこと

  • サービスクラスの実装
  • レスポンスフォームクラスの実装
  • レスポンスリザルトクラスの実装
  • レシーバクラスの実装
  • コントローラーの実装  

次回やること

  • クライアント側の実装
  • 分野による検索機能の実装

<!-- Font Awesome Free 5.0.13 by @fontawesome - https://fontawesome.com --><!-- License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) -->