Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その17 )( 図書館一覧取得 WebAPI の作成 )
概要
Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その16 )( ログイン画面の作成7 ) の続きです。
今回の手順で確認できるのは以下の内容です。
- 図書館一覧取得 WebAPI の作成
画面作成用のテンプレートHTMLファイルは Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その7 )( ログイン画面の作成 ) で作成済でした。次の図書館一覧取得 WebAPI の作成に進みます。
参照したサイト・書籍
Spring Framework Reference Documentation - 22.10 Accessing RESTful services on the Client
http://docs.spring.io/spring/docs/4.1.7.RELEASE/spring-framework-reference/htmlsingle/#rest-client-accessREST in Spring 3: RestTemplate
https://spring.io/blog/2009/03/27/rest-in-spring-3-resttemplateSpring RESTFul Client – RestTemplate Example
http://howtodoinjava.com/2015/02/20/spring-restful-client-resttemplate-example/How to Set HTTP Request Timeouts With Spring RestTemplate
http://springinpractice.com/2013/10/27/how-to-set-http-request-timeouts-with-spring-resttemplateSimple XML SERIALIZATION
http://simple.sourceforge.net/home.php千年の孤独 - AndroidでXML - Spring Andorid -
http://d.hatena.ne.jp/horafuki_taka/20110619/1308463386- Simple を使用する方法を参考にしました。
Unit Test RESTful API with Spring MVC Test Framework
https://www.jiwhiz.com/blogs/Unit_Test_RESTful_API_with_Spring_MVC_Test_Framework- MockMvc を使用して WebAPI のテストをする方法を参考にしました。
jayway/JsonPath
https://github.com/jayway/JsonPathコンピュータクワガタ - Spring MVC 3.2のJSONのテスト
http://kuwalab.hatenablog.jp/entry/2014/04/21/Spring_MVC_3.2%E3%81%AEJSON%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88- jsonPath のテストの書き方を参考にしました。
目次
- はじめに
- 1.0.x-make-webapi-getlibrary ブランチの作成
- カーリル図書館API利用のためのアプリケーションキーの取得
- アプリケーションキー保存用設定ファイルの作成 ( Git の管理対象外にする )
- Simple ライブラリを利用できるようにする
- カーリル図書館APIの図書館データベースを呼び出す CalilApiService クラスの作成
- CalilApiService クラスのテストクラスの作成、動作確認
- 図書館一覧取得 WebAPI の Controller クラスの作成
- LibraryController クラスのテストクラスの作成、動作確認
- 次回は。。。
手順
はじめに
都道府県名を URLパラメータに受け取り、カーリル図書館APIを呼び出して図書館一覧を取得後 ( この時は XML で受信します )、JSON/JSONP で返す WebAPI を作成します。URL は以下の仕様です。
http://localhost:8080/webapi/library/getLibraryList?pref=[都道府県名(例:東京都)]
Spring Boot で JSON/JSONP のレスポンスを返す WebAPI の実装と、REST クライアントの実装を試します。
1.0.x-make-webapi-getlibrary ブランチの作成
- IntelliJ IDEA で 1.0.x-make-webapi-getlibrary ブランチを作成します。
カーリル図書館API利用のためのアプリケーションキーの取得
図書館API仕様書 の「アプリケーションキーの登録」に表示されている「こちらのページでアプリケーションキーを申請してください。」のリンクをクリックして API ダッシュボード へ移動します。
「API ダッシュボード」のページにある「ログインに進む」ボタンをクリックします。
ログイン画面へ移動しますので、Twitter, facebook, Google のいずれかのアカウントでログインします。
ログインすると再び「API ダッシュボード」のページに戻ります。今度は「開発者プロフィールの登録」ボタンをクリックします。
「開発者プロフィールの登録」のページに遷移しますので、個人名、メールアドレスを入力後「同意して利用する」ボタンをクリックします。
再び「API ダッシュボード」のページに戻ります。「新しいアプリケーションの追加」ボタンをクリックします。
「カーリル・図書館API アプリケーション登録」のページに遷移しますので、アプリ名を入力後「登録する」ボタンをクリックします。
再び「API ダッシュボード」のページに戻ります。登録したアプリケーション名に対応するアプリケーションキーが表示されますので、これを使用します。
アプリケーションキー保存用設定ファイルの作成 ( Git の管理対象外にする )
src/main/resources の下に calilapi.properties を作成します ( この時 Git には Add しません )。作成後、リンク先の内容 に変更します。
.gitignore を リンク先の内容 に変更します。
Simple ライブラリを利用できるようにする
build.gradle を リンク先のその1の内容 に変更します。
Gradle projects View の左上にある「Refresh all Gradle projects」アイコンをクリックして、変更した build.gradle の内容を反映します。
カーリル図書館APIの図書館データベースAPIを呼び出す CalilApiService クラスの作成
src/main/java/ksbysample/webapp/lending/service の下に calilapi パッケージを作成します。
図書館データベースAPI の XML レスポンスを変換して格納するクラスを作成します。src/main/java/ksbysample/webapp/lending/service/calilapi の下に Libraries.java, Library.java を作成します。作成後、リンク先の内容 に変更します。
カーリル図書館APIの図書館データベースAPIを呼び出して結果を返す Service クラスを作成します。src/main/java/ksbysample/webapp/lending/service/calilapi の下に CalilApiService.java を作成します。作成後、リンク先の内容 に変更します。
CalilApiService クラスのテストクラスの作成、動作確認
src/main/java/ksbysample/webapp/lending/service/calilapi の下の CalilApiService.java で「Create Test」ダイアログを表示し、テストクラスを作成します。
src/test/java/ksbysample/webapp/lending/service/calilapi の下に CalilApiServiceTest.java が作成されます。
最初にデータを取得できているか表示して確認してみます。src/test/java/ksbysample/webapp/lending/service/calilapi の下の CalilApiServiceTest.java を リンク先のその1の内容 に変更します。
テストを実行してみます。testGetLibraryList メソッドにカーソルを移動し、コンテキストメニューを表示後「Run 'testGetLibraryList...()' with Coverage」を選択します。
テストは成功し、取得した図書館のデータも表示されました。
まずなくならないと思われる「国立国会図書館東京本館」で存在チェックするようテストを変更します。都道府県名が間違っている場合もテストします。src/test/java/ksbysample/webapp/lending/service/calilapi の下の CalilApiServiceTest.java を リンク先のその2の内容 に変更します。
テストを実行します。CalilApiServiceTest のクラス名にカーソルを移動し、コンテキストメニューを表示後「Run 'CalilApiServiceTest' with Coverage」を選択し、テストが成功することを確認します。
図書館一覧取得 WebAPI の Controller クラスの作成
最初に /webapi から始まる URL の場合には認証不要にします。src/main/java/ksbysample/webapp/lending/config の下の WebSecurityConfig.java を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending の下に webapi.common パッケージ、webapi.library パッケージを作成します。
src/main/java/ksbysample/webapp/lending/webapi/common の下に CommonWebApiResponse.java を作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/webapi/library の下に LibraryController.java を作成します。作成後、リンク先の内容 に変更します。
Tomcat を起動して WebAPI を呼び出してみます。Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
ブラウザを起動して
http://localhost:8080/webapi/library/getLibraryList?pref=%e6%9d%b1%e4%ba%ac%e9%83%bd
にアクセスします (%e6%9d%b1%e4%ba%ac%e9%83%bd
は "東京都" を UTF-8 で URLエンコードした文字列です )。東京都の図書館データベース一覧が取得できました。返ってきた JSON を整形してみます。src/main/java/ksbysample/webapp/lending/webapi/library の下に data.json というファイルを作成し、表示された JSON メッセージをコピー&ペーストで貼り付けた後、Ctrl+Alt+L で整形します。
content の中に図書館データが配列で出力されており、問題なさそうです。確認後、data.json は削除します。
Ctrl+F2 を押して Tomcat を停止します。
LibraryController クラスのテストクラスの作成、動作確認
MockMvc で JSON のテストをする時に使用する jsonpath を使用できるようにします。build.gradle を リンク先のその2の内容 に変更します。
Gradle projects View の左上にある「Refresh all Gradle projects」アイコンをクリックして、変更した build.gradle の内容を反映します。
src/main/java/ksbysample/webapp/lending/webapi/library の下の LibraryController.java で「Create Test」ダイアログを表示し、テストクラスを作成します。
- 画面下部の Member 一覧は何もチェックしません。自分でテストメソッドを書きます。
src/test/java/ksbysample/webapp/lending/webapi/library の下に LibraryControllerTest.java が作成されます。
src/test/java/ksbysample/webapp/lending/webapi/library の下の LibraryControllerTest.java を リンク先の内容 に変更します。
テストを実行します。LibraryControllerTest クラスのクラス名にカーソルを移動し、コンテキストメニューを表示後「Run 'LibraryControllerTes...' with Coverage」を選択します。
テストが成功することが確認できます。
次回は。。。
図書館一覧取得 WebAPI の作成の続きです。マージも次回行います。
- getLibraryList の JSONP 対応をします。
- RestTemplate の機能をもう少しいろいろ試してみます。XML のレスポンスを処理する方法を調べるのに結構時間がかかりましたが、JSON や JSONP の場合はもっと対応が楽なはずなので試してみたいと思います。
ソースコード
calilapi.properties
calil.apikey=(...ここにアプリケーションキーを記述します...)
.gitignore
# built application files *.apk *.ap_ # files for the dex VM *.dex # Java class files *.class # generated files bin/ gen/ # Local configuration file (sdk path, etc) local.properties # Eclipse project files .classpath .project # Proguard folder generated by Eclipse proguard/ # Intellij project files *.iml *.ipr *.iws .idea/ #Gradle .gradletasknamecache .gradle/ build/ bin/ #カーリル図書館API用設定ファイル calilapi.properties
- ファイルの最後に
calilapi.properties
を追加します。
build.gradle
■その1
dependencies { def jdbcDriver = "org.postgresql:postgresql:9.4-1201-jdbc41" // spring-boot-gradle-plugin によりバージョン番号が自動で設定されるもの // Appendix E. Dependency versions ( http://docs.spring.io/spring-boot/docs/current/reference/html/appendix-dependency-versions.html ) 参照 compile("org.springframework.boot:spring-boot-starter-web") compile("org.springframework.boot:spring-boot-starter-thymeleaf") compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity3") compile("org.springframework.boot:spring-boot-starter-data-jpa") compile("org.springframework.boot:spring-boot-starter-velocity") compile("org.springframework.boot:spring-boot-starter-mail") compile("org.springframework.boot:spring-boot-starter-security") compile("org.springframework.boot:spring-boot-starter-redis") compile("org.codehaus.janino:janino") testCompile("org.springframework.boot:spring-boot-starter-test") testCompile("org.springframework.security:spring-security-test:4.0.1.RELEASE") testCompile("org.yaml:snakeyaml") // spring-boot-gradle-plugin によりバージョン番号が自動で設定されないもの compile("${jdbcDriver}") compile("org.seasar.doma:doma:2.3.1") compile("org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16") compile("org.apache.commons:commons-lang3:3.4") compile("org.projectlombok:lombok:1.16.4") compile("com.google.guava:guava:18.0") compile("org.springframework.session:spring-session:1.0.1.RELEASE") compile("org.simpleframework:simple-xml:2.7.1") testCompile("org.dbunit:dbunit:2.5.1") testCompile("com.icegreen:greenmail:1.4.1") testCompile("org.assertj:assertj-core:3.1.0") // for Doma-Gen domaGenRuntime("org.seasar.doma:doma-gen:2.3.1") domaGenRuntime("${jdbcDriver}") }
compile("org.simpleframework:simple-xml:2.7.1")
を追加します。
■その2
dependencies { def jdbcDriver = "org.postgresql:postgresql:9.4-1201-jdbc41" // spring-boot-gradle-plugin によりバージョン番号が自動で設定されるもの // Appendix E. Dependency versions ( http://docs.spring.io/spring-boot/docs/current/reference/html/appendix-dependency-versions.html ) 参照 compile("org.springframework.boot:spring-boot-starter-web") compile("org.springframework.boot:spring-boot-starter-thymeleaf") compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity3") compile("org.springframework.boot:spring-boot-starter-data-jpa") compile("org.springframework.boot:spring-boot-starter-velocity") compile("org.springframework.boot:spring-boot-starter-mail") compile("org.springframework.boot:spring-boot-starter-security") compile("org.springframework.boot:spring-boot-starter-redis") compile("org.codehaus.janino:janino") testCompile("org.springframework.boot:spring-boot-starter-test") testCompile("org.springframework.security:spring-security-test:4.0.1.RELEASE") testCompile("org.yaml:snakeyaml") // spring-boot-gradle-plugin によりバージョン番号が自動で設定されないもの compile("${jdbcDriver}") compile("org.seasar.doma:doma:2.3.1") compile("org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16") compile("org.apache.commons:commons-lang3:3.4") compile("org.projectlombok:lombok:1.16.4") compile("com.google.guava:guava:18.0") compile("org.springframework.session:spring-session:1.0.1.RELEASE") compile("org.simpleframework:simple-xml:2.7.1") testCompile("org.dbunit:dbunit:2.5.1") testCompile("com.icegreen:greenmail:1.4.1") testCompile("org.assertj:assertj-core:3.1.0") testCompile("com.jayway.jsonpath:json-path:2.0.0") // for Doma-Gen domaGenRuntime("org.seasar.doma:doma-gen:2.3.1") domaGenRuntime("${jdbcDriver}") }
testCompile("com.jayway.jsonpath:json-path:2.0.0")
を追加します。
Libraries.java, Library.java
■Libraries.java
package ksbysample.webapp.lending.service.calilapi; import lombok.Data; import org.simpleframework.xml.ElementList; import org.simpleframework.xml.Root; import java.util.List; @Data @Root(name = "Libraries") public class Libraries { @ElementList(inline = true) private List<Library> libraryList; }
■Library.java
package ksbysample.webapp.lending.service.calilapi; import lombok.Data; import org.simpleframework.xml.Element; import org.simpleframework.xml.Root; @Data @Root(name = "Library", strict = false) public class Library { @Element private String systemid; @Element private String systemname; @Element(name = "formal") private String formalName; @Element private String address; }
- 図書館データベースAPI の XML レスポンスには上に定義した以外の要素もありますが、全ては使用しません。@Root アノテーションに
strict = false
を記述して全ての要素を定義していなくてもエラーにならないようにします。
CalilApiService.java
package ksbysample.webapp.lending.service.calilapi; import org.simpleframework.xml.Serializer; import org.simpleframework.xml.core.Persister; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.PropertySource; import org.springframework.http.ResponseEntity; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service @PropertySource("classpath:calilapi.properties") public class CalilApiService { private int CONNECT_TIMEOUT = 5000; private int READ_TIMEOUT = 5000; private final String URL_CALILAPI_LIBRALY = "http://api.calil.jp/library?appkey={appkey}&pref={pref}"; @Value("${calil.apikey}") private String calilApiKey; public Libraries getLibraryList(String pref) throws Exception { // 図書館データベースAPIを呼び出して XMLレスポンスを受信する RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory()); ResponseEntity<String> response = restTemplate.getForEntity(URL_CALILAPI_LIBRALY, String.class, this.calilApiKey, pref); // 受信した XMLレスポンスを Javaオブジェクトに変換する Serializer serializer = new Persister(); Libraries libraries = serializer.read(Libraries.class, response.getBody()); return libraries; } private ClientHttpRequestFactory getClientHttpRequestFactory() { // 接続タイムアウト、受信タイムアウトを 5秒に設定する SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(CONNECT_TIMEOUT); factory.setReadTimeout(READ_TIMEOUT); return factory; } }
- calilapi.properties に記述したアプリケーションキーを利用するために以下のように実装します。
- クラスに
@PropertySource("classpath:calilapi.properties")
アノテーションを記述します。 - クラス内に
@Value("${calil.apikey}") private String calilApiKey;
を記述し、calilapi.properties 内の calil.apikey の値を calilApiKey フィールドに読み込みます。
- クラスに
- RestTemplate で受信したレスポンスは一旦 String に保存し、その後に Simple ライブラリを利用して Java オブジェクトに変換します。
CalilApiServiceTest.java
■その1
package ksbysample.webapp.lending.service.calilapi; import ksbysample.webapp.lending.Application; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class CalilApiServiceTest { @Autowired private CalilApiService calilApiService; @Test public void testGetLibraryList() throws Exception { Libraries libraries = calilApiService.getLibraryList("東京都"); libraries.getLibraryList().stream() .forEach(s -> System.out.println(s.getSystemid() + "," + s.getFormalName() + "," + s.getAddress())); } }
■その2
package ksbysample.webapp.lending.service.calilapi; import ksbysample.webapp.lending.Application; import org.assertj.core.api.ThrowableAssert; import org.junit.Test; import org.junit.runner.RunWith; import org.simpleframework.xml.core.ValueRequiredException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.StrictAssertions.assertThatThrownBy; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class CalilApiServiceTest { @Autowired private CalilApiService calilApiService; @Test public void testGetLibraryList_都道府県名が正しい場合() throws Exception { Libraries libraries = calilApiService.getLibraryList("東京都"); // systemname が "国立国会図書館" のデータがあるかチェックする assertThat(libraries.getLibraryList()) .extracting("systemname") .contains("国立国会図書館"); } @Test public void testGetLibraryList_都道府県名が間違っている場合() throws Exception { assertThatThrownBy(new ThrowableAssert.ThrowingCallable() { @Override public void call() throws Throwable { Libraries libraries = calilApiService.getLibraryList("東a京都"); } }).isInstanceOf(ValueRequiredException.class); } }
WebSecurityConfig.java
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 認証の対象外にしたいURLがある場合には、以下のような記述を追加します // 複数URLがある場合はantMatchersメソッドにカンマ区切りで対象URLを複数列挙します // .antMatchers("/country/**").permitAll() .antMatchers("/fonts/**").permitAll() .antMatchers("/html/**").permitAll() .antMatchers("/encode").permitAll() .antMatchers("/urllogin").permitAll() .antMatchers("/webapi/**").permitAll() .anyRequest().authenticated(); http.formLogin() .loginPage("/") .loginProcessingUrl("/login") .defaultSuccessUrl(WebSecurityConfig.DEFAULT_SUCCESS_URL) .failureUrl("/") .usernameParameter("id") .passwordParameter("password") .permitAll() .and() .logout() .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) .logoutSuccessUrl("/") .deleteCookies("JSESSIONID") .deleteCookies("remember-me") .invalidateHttpSession(true) .permitAll() .and() .rememberMe() .key(REMEMBERME_KEY) .tokenValiditySeconds(60 * 60 * 24 * 30); }
.antMatchers("/webapi/**").permitAll()
を追加します。
CommonWebApiResponse.java
package ksbysample.webapp.lending.webapi.common; import lombok.Data; @Data public class CommonWebApiResponse<T> { private String errcode; private String errmsg; private T content; }
LibraryController.java
package ksbysample.webapp.lending.webapi.library; import ksbysample.webapp.lending.service.calilapi.CalilApiService; import ksbysample.webapp.lending.service.calilapi.Libraries; import ksbysample.webapp.lending.service.calilapi.Library; import ksbysample.webapp.lending.webapi.common.CommonWebApiResponse; import org.simpleframework.xml.core.ValueRequiredException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Collections; import java.util.List; @RestController @RequestMapping("/webapi/library") public class LibraryController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private CalilApiService calilApiService; @RequestMapping("/getLibraryList") public CommonWebApiResponse<List<Library>> getLibraryList(String pref) throws Exception { CommonWebApiResponse<List<Library>> response = new CommonWebApiResponse<>(); response.setContent(Collections.EMPTY_LIST); try { Libraries libraries = calilApiService.getLibraryList(pref); response.setContent(libraries.getLibraryList()); } catch (ValueRequiredException e) { response.setErrcode(-2); response.setErrmsg("都道府県名が正しくありません。"); } catch (Exception e) { logger.error("システムエラーが発生しました。", e); response.setErrcode(-1); response.setErrmsg("システムエラーが発生しました。"); } return response; } }
LibraryControllerTest.java
package ksbysample.webapp.lending.webapi.library; import ksbysample.common.test.SecurityMockMvcResource; import ksbysample.webapp.lending.Application; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.Matchers.hasSize; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class LibraryControllerTest { @Rule @Autowired public SecurityMockMvcResource mvc; @Test public void 正しい都道府県を指定した場合には図書館一覧が返る() throws Exception { mvc.noauth.perform(get("/webapi/library/getLibraryList?pref=東京都")) .andExpect(status().isOk()) .andExpect(content().contentType("application/json;charset=UTF-8")) .andExpect(jsonPath("$.errcode", is(0))) .andExpect(jsonPath("$.errmsg", is(""))) .andExpect(jsonPath("$.content[0].address", startsWith("東京都"))) .andExpect(jsonPath("$.content[?(@.formalName=='国立国会図書館東京本館')]").exists()); } @Test public void 間違った都道府県を指定した場合にはエラーが返る() throws Exception { mvc.noauth.perform(get("/webapi/library/getLibraryList?pref=東a京都")) .andExpect(status().isOk()) .andExpect(content().contentType("application/json;charset=UTF-8")) .andExpect(jsonPath("$.errcode", is(-2))) .andExpect(jsonPath("$.errmsg", is("都道府県名が正しくありません。"))) .andExpect(jsonPath("$.content", hasSize(0))); } }
履歴
2015/08/19
初版発行。
2015/08/23
* LibraryControllerTest のテストの実行結果を書き忘れていたので追記しました。