読者です 読者をやめる 読者になる 読者になる

かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( 番外編 )( RestTemplate&WebAPI をいろいろ試してみる )

概要

Spring Framework の RestTemplate で WebAPI を呼び出したり、JSON 等でレスポンスを返したりするのがかなり簡単で気に入ったので、いろいろ試してみたいと思います。

  • 今回の手順で確認できるのは以下の内容です。
    • RestTemplate で JSONP を返す WebAPI を呼び出してみる
    • WebAPI で XML でデータを返す
    • RestTemplate でカーリル図書館APIの図書館データベースAPIを呼び出して、XML のレスポンスを jackson-dataformat-xml で処理する

参照したサイト・書籍

  1. Spring 4 MVC REST Service Example using @RestController
    http://websystique.com/springmvc/spring-4-mvc-rest-service-example-using-restcontroller/

  2. FasterXML/jackson-dataformat-xml
    https://github.com/FasterXML/jackson-dataformat-xml

    • JAXB のアノテーションを使いつつ、内部では Jackson で XML を取り扱えるようになるライブラリ、だと思います。実は内部の仕組みがよく分かっていません。。。
    • build.gradle に compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.5.3") を記述して、使用される ObjectMapper クラスで findAndRegisterModules メソッドを呼び出して認識させるだけでよく 、XML の出力が格段に楽になるので、個人的には気に入りました。
  3. CLOVER - JAXBをXML Schemaなしで使ってみる
    http://d.hatena.ne.jp/Kazuhira/20120716/1342435297

目次

  1. 1.0.x-try-resttemplate-and-webapi ブランチの作成
  2. RestTemplate で JSONP を返す WebAPI を呼び出してみる
  3. WebAPI で XML でデータを返す
  4. RestTemplate でカーリル図書館APIの図書館データベースAPIを呼び出して、XML のレスポンスを jackson-dataformat-xml で処理する
    1. 変換先の Java クラスを作成し、RestTemplate で図書館データベースAPIを呼び出してみる
    2. <Library>...</Library> が1件も取り込まれなかった原因を調べる
    3. <Library>...</Library> を List へ取り込めるようにする
    4. RestTemplate の getForEntity メソッドからダイレクトに LibrariesForJackson2Xml クラスに変換する
    5. <Library>...</Library> が1件も取り込まれなかった原因を調べる ( その2 )
    6. RestTemplate の getForEntity メソッドからダイレクトに LibrariesForJackson2Xml クラスに変換する ( その2 )
  5. commit、Push、Pull Request、マージ
  6. @RestController&RestTemplate の XML 対応をまとめてみる

手順

1.0.x-try-resttemplate-and-webapi ブランチの作成

  1. IntelliJ IDEA で 1.0.x-try-resttemplate-and-webapi ブランチを作成します。

RestTemplate で JSONP を返す WebAPI を呼び出してみる

RestTemplate には JSONP を自動で解析してくれる機能はないようなので、レスポンスを String で受け取り、処理内で関数名の部分を取り除くようにします。

  1. src/main/java/ksbysample/webapp/lending/service/openweathermapapi の下の OpenWeatherMapApiService.javaリンク先の内容 に変更します。

  2. テストクラスを作成して動作を確認します。src/test/java/ksbysample/webapp/lending/service/openweathermapapi の下の OpenWeatherMapApiServiceTest.javaリンク先の内容 に変更します。

  3. テストを実行してみます。testGetFiveDayThreeHourForecastByJSONP メソッドにカーソルを移動し、コンテキストメニューを表示後「Run 'testGetFiveDayThre...()' with Coverage」を選択します。

    テストが成功することが確認できます。

    f:id:ksby:20150826153526p:plain

  4. System.out.println で出力されたデータの内容が問題ないかも確認した後、testGetFiveDayThreeHourForecastByJSONP メソッドの System.out.println している部分をコメントアウトします。

  5. 一旦ここで commit します。

WebAPI で XML でデータを返す

  1. JSON で返す場合には @RestController アノテーションを付加したクラスから Java オブジェクトを return しましたが ( 自動で Jackson により Java オブジェクトが JSON に変換されます )、XML で返す場合には JAXB のアノテーションを付加したクラスを作成し、そのクラスのオブジェクトを return します。

    ただし実際に試してみたところ以下のような問題があり Jackson で JSON に出力する場合と比較すると使いづらい感じがしました。

    • Map<String, String> のデータを出力しようとすると、<key>value</key> と出力されて欲しいところが <entry><key>...</key><value>...</value></entry> と出力される。

      f:id:ksby:20150829015536p:plain

    • LocalDateTime のデータを適切に出力できない。

  2. Jackson と同じような使い勝手で XML を出力できないか調べたところ、FasterXML/jackson-dataformat-xml というライブラリがありましたので、それを使用することにします。

    • build.gradle にライブラリの記述をするだけでよく、Java クラスには JAXB のアノテーションをそのまま使用することができます。
    • Map<String, String> のデータを出力する時に <key>value</key> のフォーマットで出力されるようになります。
    • FasterXML/jackson-datatype-jsr310 と組み合わせることで、LocalDateTime のデータを指定したフォーマットで出力できるようになります。
  3. build.gradle を リンク先の内容 に変更します。

  4. Gradle projects View の左上にある「Refresh all Gradle projects」アイコンをクリックして、変更した build.gradle の内容を反映します。

  5. 作成した OpenWeatherMapApiService クラスの getFiveDayThreeHourForecast メソッドで取得したFiveDayThreeHourForecastData クラスのデータのうち、以下の内容を XML で戻す WebAPI を作成します。

    • FiveDayThreeHourForecastData クラスのフィールドの内、city, cnt, list のみ返す。
    • city のデータは全て返さない。name, coord, country のみ返す。
    • list の ForecastData クラスのフィールドは全て返さず、dt, weather, wind, dt_txt のみ返す。
  6. src/main/ksbysample/webapp/lending/webapi の下に weather パッケージを作成します。

  7. src/main/ksbysample/webapp/lending/webapi/weather の下に CityResponse.java を作成します。作成後、リンク先の内容 に変更します。

  8. src/main/ksbysample/webapp/lending/webapi/weather の下に ForecastResponse.java を作成します。作成後、リンク先の内容 に変更します。

  9. src/main/ksbysample/webapp/lending/webapi/weather の下に FiveDayThreeHourForecastResponse.java を作成します。作成後、リンク先の内容 に変更します。

  10. src/main/ksbysample/webapp/lending/webapi/weather の下に WeatherController.java を作成します。作成後、リンク先の内容 に変更します。

  11. 動作確認します。Gradle projects View から bootRun タスクを実行して Tomcat を起動します。

  12. ブラウザを起動して http://localhost:8080/webapi/weather/getFiveDayThreeHourForecast?cityname=Tokyo にアクセスします。

    レスポンスとして XML データが返り、ブラウザ上に表示されます。

    f:id:ksby:20150829133504p:plain f:id:ksby:20150829133909p:plain

    http://api.openweathermap.org/data/2.5/forecast?q=Tokyo から取得した元データ ( JSON ) も以下に記載します。JSON のデータが XML の方に出力されていることが確認できます。

    f:id:ksby:20150829134119p:plain f:id:ksby:20150829134418p:plain

  13. Ctrl+F2 を押して Tomcat を停止します。

  14. 一旦ここで commit します。

RestTemplate でカーリル図書館APIの図書館データベースAPIを呼び出して、XML のレスポンスを jackson-dataformat-xml で処理する

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その17 )( 図書館一覧取得 WebAPI の作成 ) では XMLJava オブジェクトへの変換に Simple ライブラリを使用しましたが、FasterXML/jackson-dataformat-xml を使用すれば Jackson で XML を処理できるようになるようなので、Jackson で XMLJava オブジェクトへの変換ができるか試してみます。

変換先の Java クラスを作成し、RestTemplate で図書館データベースAPIを呼び出してみる

  1. src/main/java/ksbysample/webapp/lending/service/calilapi の下に LibraryForJackson2Xml.java を作成します。作成後、リンク先の内容 に変更します。

  2. src/main/java/ksbysample/webapp/lending/service/calilapi の下に LibrariesForJackson2Xml.java を作成します。作成後、リンク先のその1の内容 に変更します。

  3. src/main/java/ksbysample/webapp/lending/service/calilapi の下の CalilApiService.javaリンク先のその1の内容 に変更します。

  4. 動作確認するためにテストメソッドを作成します。src/test/java/ksbysample/webapp/lending/service/calilapi の下の CalilApiServiceTest.javaリンク先のその1の内容 に変更します。

  5. テストを実行してみます。testGetLibraryListByJackson2Xml メソッドにカーソルを移動し、コンテキストメニューを表示後「Run 'testGetLibraryList...()' with Coverage」を選択します。

    テストは成功しますが、XMLJava オブジェクトへの変換が出来ていませんでした。<Library>...</Library> の部分が1件も取り込まれていません。原因を調査します。

    f:id:ksby:20150830170224p:plain

<Library>...</Library> が1件も取り込まれなかった原因を調べる

今のままだと調査しづらいので、Simple ライブラリの時のように一旦図書館データベースAPIXML レスポンスを String へ入れてから変換するようにします。src/main/java/ksbysample/webapp/lending/service/calilapi の下の CalilApiService.javaリンク先のその2の内容 に変更します。

再度テストを実行します。今度は com.fasterxml.jackson.databind.JsonMappingException: Can not instantiate value of type [simple type, class ksbysample.webapp.lending.service.calilapi.LibraryForJackson2Xml] from String value ('Special_Aacf'); no single-String constructor/factory method というエラーメッセージが出力されました。

f:id:ksby:20150830171949p:plain

エラーメッセージの原因を調べて分かったことは ( 全然分からなくてかなり時間がかかりました。。。 )、図書館データベースAPIXML レスポンスは以下の形式で返ってきますが、

<Libraries>
  <Library>
    <systemid>Special_Aacf</systemid>
    ....
  </Library>
  <Library>
    ....
  </Library>
  .....
</Libraries>

RootElement 直下の <Library>...</Library> が繰り返される部分を以下の書き方で List にセットすることはできない、ということでした。

@XmlElement(name = "Library")  
private List<LibraryForJackson2Xml> libraryList;  

List ではなく以下のように書くと、一番最後の <Library>...</Library> のデータが libraryList にセットされます。

@XmlElement(name = "Library")  
private LibraryForJackson2Xml libraryList;  

f:id:ksby:20150830173454p:plain

XML が以下のような形式であれば、

<Libraries>
  <LibraryList>
    <Library>
      <systemid>Special_Aacf</systemid>
      ....
    </Library>
    <Library>
      ....
    </Library>
    .....
  </LibraryList>
</Libraries>

以下の書き方で List に取り込めるようです。

@XmlElementWrapper(name = "LibraryList")
@XmlElement(name = "Library")  
private List<LibraryForJackson2Xml> libraryList;  

Simple ライブラリを使った時も @ElementList(inline = true) のように inline = true という定義を追加して取り込めるようになっていましたが、RootElement 直下に同じ Element 名のデータが列挙されると JAXB の @XmlElement アノテーションを単に付けただけでは List に取り込めない、という原因でした。

@XmlElement アノテーションはフィールドだけでなくメソッドにも付加できることが調査中に分かりましたので、メソッドに付加して、メソッドが呼び出されたらリストに追加するように変更します。

<Library>...</Library> を List へ取り込めるようにする

  1. src/main/java/ksbysample/webapp/lending/service/calilapi の下の LibrariesForJackson2Xml.javaリンク先のその2の内容 に変更します。

  2. <Library>...</Library> のデータが取り込めているか確認しやすいようにするためにテストメソッドを変更します。src/test/java/ksbysample/webapp/lending/service/calilapi の下の CalilApiServiceTest.javaリンク先のその2の内容 に変更します。

  3. テストを実行します。testGetLibraryListByJackson2Xml メソッドにカーソルを移動し、コンテキストメニューを表示後「Run 'testGetLibraryList...()' with Coverage」を選択します。

    今度は <Library>...</Library> のデータを正常に取り込めました。

    f:id:ksby:20150830175851p:plain

RestTemplate の getForEntity メソッドからダイレクトに LibrariesForJackson2Xml クラスに変換する

  1. RestTemplate の getForEntity メソッドからダイレクトに LibrariesForJackson2Xml クラスに変換してみます。src/main/java/ksbysample/webapp/lending/service/calilapi の下の CalilApiService.javaリンク先のその1の内容 に戻します。

  2. テストを実行します。testGetLibraryListByJackson2Xml メソッドにカーソルを移動し、コンテキストメニューを表示後「Run 'testGetLibraryList...()' with Coverage」を選択します。

    なぜか <Library>...</Library> の部分が1件も取り込まれていませんでした。。。 原因を調査します。

    f:id:ksby:20150830184451p:plain

<Library>...</Library> が1件も取り込まれなかった原因を調べる ( その2 )

  • デバッガでソースの中を追って調べるしかないかと思い、調べてみましたが全然分かりませんでした。。。 ただし RestTemplate の MessageConverter には MappingJackson2XmlHttpMessageConverter が使用されていることは分かりました。
  • 取り込まれた時と取り込まれていない時の実装を比較してみると、
    • MappingJackson2XmlHttpMessageConverter を使用しているので、XmlMapper が使用されているという考えで間違いないはず。
    • そうすると考えられるのは ObjectMapper クラスの findAndRegisterModules メソッドが呼び出されておらず、変換時に jackson-dataformat-xml が有効になっていない???
    • jackson-datatype-jsr310 の時は何もしなくても良かったので同じものと考えていたのですが、MappingJackson2XmlHttpMessageConverter が使用される時は設定されていないのかもしれません。

ということで RestTemplate に MessageConverter を設定するよう修正します。

RestTemplate の getForEntity メソッドからダイレクトに LibrariesForJackson2Xml クラスに変換する ( その2 )

  1. src/main/java/ksbysample/webapp/lending/service/calilapi の下の CalilApiService.javaリンク先のその3の内容 に変更します。

  2. テストを実行します。testGetLibraryListByJackson2Xml メソッドにカーソルを移動し、コンテキストメニューを表示後「Run 'testGetLibraryList...()' with Coverage」を選択します。

    getForEntity メソッドからダイレクトに LibrariesForJackson2Xml クラスに変換して <Library>...</Library> のデータを正常に取り込めるようになりました。

    f:id:ksby:20150830192311p:plain

  3. テストメソッドを「testGetLibraryList_都道府県名が正しい場合()」メソッドに合わせます。src/test/java/ksbysample/webapp/lending/service/calilapi の下の CalilApiServiceTest.javaリンク先のその3の内容 に変更します。テストも実行して成功することを確認します。

  4. 一旦ここで commit します。

commit、Push、Pull Request、マージ

  1. コマンドラインから git rebase コマンドで commit を1つにまとめます。

  2. GitHub へ Push、1.0.x-try-resttemplate-and-webapi -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-try-resttemplate-and-webapi ブランチを削除、をします。

@RestController&RestTemplate の XML 対応をまとめてみる

今まで調査・実装した結果をまとめておきます。詳しく使い込んでいる訳ではないので間違っている部分もあるかもしれません。

  • JSON のみで OK ならば XML なんて取り扱わない方がいいです。Jackson & JSON は使い勝手もよく、Spring Boot & Spring Framework のサポートも秀逸です。今ってこんな簡単に実装できてしまうんだ、と本当に思いました。
  • JAXB の使い勝手が悪い気がします。標準仕様なのかもしれませんが、Jackson の使い勝手がよいので避けられるなら避けたいですね。
  • @RestController アノテーションを付加したクラスから XML データを返す場合の感想です。
    • jackson-dataformat-xml を使ってやるのが一番楽だと思います。Java オブジェクト → XML 変換には RestTemplate の時のような苦労はなく Jackson & JSON の時と同じ感じで使える印象です。
    • Simple ライブラリが使えれば楽なのですが、標準では対応していないと思います。Web で調べていた時には Spring for Android と一緒に見かけたのでこれがあればよさそうな気がしますが、試していないので分かりません。
  • RestTemplate で XML データを返す WebAPI を呼び出す場合の感想です。
    • jackson-dataformat-xml を使えば JSON の時とある程度同様の使い勝手が得られるのですが、JAXB の仕様の使い勝手の悪さに一部引きずられている気がします。うまく動かない時の調査はすごく大変でした。
    • Simple ライブラリは JAXB の仕様を引きずっておらず、使いやすいライブラリでした。可能ならばこのライブラリを使用することをお勧めします。ただし RestController で返す処理も実装する場合には、RestTemplate の時は Simple ライブラリ、RestController の時は jackson-dataformat-xml になるのはあまり良い気はしないので、その場合には jackson-dataformat-xml で揃えた方がよいのかな。。。

ソースコード

OpenWeatherMapApiService.java

package ksbysample.webapp.lending.service.openweathermapapi;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
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;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Service
public class OpenWeatherMapApiService {

    private int CONNECT_TIMEOUT = 5000;
    private int READ_TIMEOUT = 5000;

    private final String URL_WEATHERAPI_5DAY3HOURFORECAST = "http://api.openweathermap.org/data/2.5/forecast?q={cityname}";

    public FiveDayThreeHourForecastData getFiveDayThreeHourForecast(String cityname) {
        RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory());
        ResponseEntity<FiveDayThreeHourForecastData> response
                = restTemplate.getForEntity(URL_WEATHERAPI_5DAY3HOURFORECAST, FiveDayThreeHourForecastData.class, cityname);
        return response.getBody();
    }

    public FiveDayThreeHourForecastData getFiveDayThreeHourForecastByJSONP(String cityname, String callback) throws IOException {
        assert(StringUtils.isNotBlank(callback));

        // callback パラメータを付加して WebAPI を呼び出し、レスポンスを String で受け取る
        // ※今回は RestTemplate に Map でパラメータを渡す
        RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory());
        String url = URL_WEATHERAPI_5DAY3HOURFORECAST + "&callback={callback}";
        Map<String, String> vars = new HashMap<>();
        vars.put("cityname", cityname);
        vars.put("callback", callback);
        ResponseEntity<String> response = restTemplate.getForEntity(url, String.class, vars);

        // レスポンスの文字列から "callback関数名(" + ");" の部分を削除して JSON 文字列を抽出する
        String body = response.getBody();
        StringBuilder json = new StringBuilder(body.length() - callback.length() - 3);
        json.append(body.substring(callback.length() + 1, body.length() - 3));
        
        // JSON 文字列から Java オブジェクトへ変換する
        // ※自分で ObjectMapper オブジェクトを生成した時には findAndRegisterModules メソッドを呼び出さないと
        //   jackson-datatype-jsr310 による LocalDateTime 変換が行われない
        ObjectMapper mapper = new ObjectMapper();
        mapper.findAndRegisterModules();
        FiveDayThreeHourForecastData fiveDayThreeHourForecastData
                = mapper.readValue(json.toString(), FiveDayThreeHourForecastData.class); 
        
        return fiveDayThreeHourForecastData;
    }

    private ClientHttpRequestFactory getClientHttpRequestFactory() {
        // 接続タイムアウト、受信タイムアウトを 5秒に設定する
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(CONNECT_TIMEOUT);
        factory.setReadTimeout(READ_TIMEOUT);
        return factory;
    }

}
  • getFiveDayThreeHourForecastByJSONP メソッドを追加します。

OpenWeatherMapApiServiceTest.java

package ksbysample.webapp.lending.service.openweathermapapi;

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;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class OpenWeatherMapApiServiceTest {

    @Autowired
    private OpenWeatherMapApiService openWeatherMapApiService;
    
    @Test
    public void testGetFiveDayThreeHourForecast() throws Exception {
        FiveDayThreeHourForecastData fiveDayThreeHourForecastData
                = openWeatherMapApiService.getFiveDayThreeHourForecast("Tokyo");
        assertThat(fiveDayThreeHourForecastData.getCity().getName()).isEqualTo("Tokyo");
        assertThat(fiveDayThreeHourForecastData.getCity().getCountry()).isEqualTo("JP");
        assertThat(fiveDayThreeHourForecastData.getList().size()).isGreaterThan(0);
//        System.out.println(fiveDayThreeHourForecastData);
    }

    @Test
    public void testGetFiveDayThreeHourForecastByJSONP() throws Exception {
        FiveDayThreeHourForecastData fiveDayThreeHourForecastData
                = openWeatherMapApiService.getFiveDayThreeHourForecastByJSONP("Tokyo", "func");
        assertThat(fiveDayThreeHourForecastData.getCity().getName()).isEqualTo("Tokyo");
        assertThat(fiveDayThreeHourForecastData.getCity().getCountry()).isEqualTo("JP");
        assertThat(fiveDayThreeHourForecastData.getList().size()).isGreaterThan(0);
        System.out.println(fiveDayThreeHourForecastData);
    }

}
  • testGetFiveDayThreeHourForecastByJSONP メソッドを追加します。

build.gradle

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")
    // (ここから) gradle でテストを実行した場合に spring-security-test-4.0.1.RELEASE.jar しか classpath に指定されず
    // テストが失敗したため、3.2.7.RELEASE を明記している
    testCompile("org.springframework.security:spring-security-core:3.2.7.RELEASE")
    testCompile("org.springframework.security:spring-security-web:3.2.7.RELEASE")
    // (ここまで) ------------------------------------------------------------------------------------------------------
    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")
    compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.6.1")
    compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.5.3")
    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}")
}
  • compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.5.3") を追加します。

CityResponse.java

package ksbysample.webapp.lending.webapi.weather;

import ksbysample.webapp.lending.service.openweathermapapi.CityData;
import lombok.Data;
import org.springframework.beans.BeanUtils;

import java.util.Map;

@Data
public class CityResponse {

    private String name;

    private Map<String, String> coord;

    private String country;
    
    CityResponse(CityData cityData) {
        BeanUtils.copyProperties(cityData, this);
    }
    
}

ForecastResponse.java

package ksbysample.webapp.lending.webapi.weather;

import com.fasterxml.jackson.annotation.JsonFormat;
import ksbysample.webapp.lending.service.openweathermapapi.ForecastData;
import lombok.Data;
import org.springframework.beans.BeanUtils;

import javax.xml.bind.annotation.XmlElement;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

@Data
public class ForecastResponse {

    private BigDecimal dt;

    private List<Map<String, String>> weather;

    private Map<String, String> wind;

    @XmlElement(name = "dt_txt")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime dtTxt;
    
    ForecastResponse(ForecastData forecastData) {
        BeanUtils.copyProperties(forecastData, this);
    }
    
}
  • 出力時の項目名を JSON の "dt_txt" と合わせるために @XmlElement(name = "dt_txt") を付加しています。
  • LocalDateTime のフィールドの値をフォーマットするために @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") を付加しています。XML で出力する時も JSON の時と同じ @JsonFormat アノテーションを使用します。
  • JSON 用データクラス ForecastData からデータをコピーするためのコンストラクタを追加します。

FiveDayThreeHourForecastResponse.java

package ksbysample.webapp.lending.webapi.weather;

import ksbysample.webapp.lending.service.openweathermapapi.FiveDayThreeHourForecastData;
import lombok.Data;
import org.springframework.beans.BeanUtils;

import javax.xml.bind.annotation.XmlRootElement;
import java.util.List;
import java.util.stream.Collectors;

@XmlRootElement(name = "FiveDayThreeHourForecastData")
@Data
public class FiveDayThreeHourForecastResponse {

    private CityResponse city;

    private int cnt;

    private List<ForecastResponse> list;

    FiveDayThreeHourForecastResponse(FiveDayThreeHourForecastData fiveDayThreeHourForecastData) {
        BeanUtils.copyProperties(fiveDayThreeHourForecastData, this, "city", "list");
        city = new CityResponse(fiveDayThreeHourForecastData.getCity());
        list = fiveDayThreeHourForecastData.getList().stream()
                .map(ForecastResponse::new)
                .collect(Collectors.toList());
    }

}
  • JAXB の @XmlRootElement アノテーションを付加します。
  • JSON 用データクラス FiveDayThreeHourForecastData からデータをコピーするためのコンストラクタを追加します。
  • 本来 JAXB のアノテーションを付加したクラスには引数がないコンストラクタを作成する必要がありますが、内部の処理を Jackson がしているので作成する必要はありません。

WeatherController.java

package ksbysample.webapp.lending.webapi.weather;

import ksbysample.webapp.lending.service.openweathermapapi.FiveDayThreeHourForecastData;
import ksbysample.webapp.lending.service.openweathermapapi.OpenWeatherMapApiService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/webapi/weather")
public class WeatherController {

    @Autowired
    private OpenWeatherMapApiService openWeatherMapApiService;
    
    @RequestMapping("/getFiveDayThreeHourForecast")
    public FiveDayThreeHourForecastResponse getFiveDayThreeHourForecast(String cityname) {
        FiveDayThreeHourForecastData fiveDayThreeHourForecastData
                = openWeatherMapApiService.getFiveDayThreeHourForecast(cityname);
        FiveDayThreeHourForecastResponse fiveDayThreeHourForecastResponse
                = new FiveDayThreeHourForecastResponse(fiveDayThreeHourForecastData);
        return fiveDayThreeHourForecastResponse;
    }

}

LibraryForJackson2Xml.java

package ksbysample.webapp.lending.service.calilapi;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import lombok.ToString;

@JsonIgnoreProperties(ignoreUnknown = true)
@Data
@ToString
public class LibraryForJackson2Xml {

    private String systemid;

    private String systemname;

    @XmlElement(name = "formal")
    private String formalName;

    private String address;
    
}
  • 定義していない項目は無視するために @JsonIgnoreProperties(ignoreUnknown = true) を付加します。
  • データ確認用に lombok の @ToString アノテーションを付加します。

LibrariesForJackson2Xml.java

■その1

package ksbysample.webapp.lending.service.calilapi;

import lombok.Data;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.List;

@XmlRootElement(name = "Libraries")
@Data
public class LibrariesForJackson2Xml {

    @XmlElement(name = "Library")
    private List<LibraryForJackson2Xml> libraryList;

}

■その2

package ksbysample.webapp.lending.service.calilapi;

import lombok.Data;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.ArrayList;
import java.util.List;

@XmlRootElement(name = "Libraries")
@Data
public class LibrariesForJackson2Xml {

    private List<LibraryForJackson2Xml> libraryList = new ArrayList<>();

    @XmlElement(name = "Library")
    public void addLibraryList(LibraryForJackson2Xml library) {
        libraryList.add(library);
    }    
    
}
  • addLibraryList メソッドを追加し、このメソッド@XmlElement(name = "Library") を付加します。これで <Library>...</Library> が現れる度にこのメソッドが呼び出されるようになります。

CalilApiService.java

■その1

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;
    }

    public LibrariesForJackson2Xml getLibraryListByJackson2Xml(String pref) throws Exception {
        // 図書館データベースAPIを呼び出して XMLレスポンスを受信する
        RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory());
        ResponseEntity<LibrariesForJackson2Xml> response
                = restTemplate.getForEntity(URL_CALILAPI_LIBRALY, LibrariesForJackson2Xml.class, this.calilApiKey, pref);
        return response.getBody();
    }

    private ClientHttpRequestFactory getClientHttpRequestFactory() {
        // 接続タイムアウト、受信タイムアウトを 5秒に設定する
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(CONNECT_TIMEOUT);
        factory.setReadTimeout(READ_TIMEOUT);
        return factory;
    }
    
}
  • getLibraryListByJackson2Xml メソッドを追加します。XMLJava オブジェクトへ自動変換される想定で、単に RestTemplate の getForEntity メソッドを呼び出すだけにしています。

■その2

    public LibrariesForJackson2Xml getLibraryListByJackson2Xml(String pref) throws Exception {
        // 図書館データベースAPIを呼び出して XMLレスポンスを受信する
        RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory());
        ResponseEntity<String> response
                = restTemplate.getForEntity(URL_CALILAPI_LIBRALY, String.class, this.calilApiKey, pref);

        ObjectMapper mapper = new XmlMapper();
        mapper.findAndRegisterModules();
        LibrariesForJackson2Xml libraries = mapper.readValue(response.getBody(), LibrariesForJackson2Xml.class);

        return libraries;
    }
  • getLibraryListByJackson2Xml メソッドを上記の内容に変更します。
    • restTemplate.getForEntity では String.class で結果を受け取るようにします。
    • その後、XmlMapper のインスタンスを生成して readValue メソッドXMLJava オブジェクトへ変換します。

■その3

package ksbysample.webapp.lending.service.calilapi;

import com.fasterxml.jackson.dataformat.xml.XmlMapper;
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.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.stereotype.Service;
import org.springframework.util.ClassUtils;
import org.springframework.web.client.RestTemplate;

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

@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;
    }

    public LibrariesForJackson2Xml getLibraryListByJackson2Xml(String pref) throws Exception {
        // 図書館データベースAPIを呼び出して XMLレスポンスを受信する
        RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory());
        restTemplate.setMessageConverters(getMessageConvertersforJackson2XML());
        ResponseEntity<LibrariesForJackson2Xml> response
                = restTemplate.getForEntity(URL_CALILAPI_LIBRALY, LibrariesForJackson2Xml.class, this.calilApiKey, pref);
        return response.getBody();
    }

    private ClientHttpRequestFactory getClientHttpRequestFactory() {
        // 接続タイムアウト、受信タイムアウトを 5秒に設定する
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(CONNECT_TIMEOUT);
        factory.setReadTimeout(READ_TIMEOUT);
        return factory;
    }

    private List<HttpMessageConverter<?>> getMessageConvertersforJackson2XML() {
        // build.gralde に compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:...") を記述して jackson-dataformat-xml
        // が使用できるようになっていない場合にはエラーにする
        assert(ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", RestTemplate.class.getClassLoader()));

        MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter
                = new MappingJackson2XmlHttpMessageConverter();
        // findAndRegisterModules メソッドを呼び出して jackson-dataformat-xml が機能するようにする
        mappingJackson2XmlHttpMessageConverter.setObjectMapper(new XmlMapper().findAndRegisterModules());

        List<MediaType> mediaTypes = new ArrayList<>();
        mediaTypes.add(MediaType.APPLICATION_XML);
        mediaTypes.add(MediaType.TEXT_XML);
        mappingJackson2XmlHttpMessageConverter.setSupportedMediaTypes(mediaTypes);

        List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
        messageConverters.add(mappingJackson2XmlHttpMessageConverter);
        return messageConverters;
    }
    
}
  • getMessageConvertersforJackson2XML を追加します。assert の部分は RestTemplate クラスのソースを見て入れました。このメソッドは jackson-dataformat-xml が有効な時だけ使えるようにしています。
  • getLibraryListByJackson2Xml メソッド内の処理に restTemplate.setMessageConverters(getMessageConvertersforJackson2XML()); を追加します。

CalilApiServiceTest.java

■その1

    @Test
    public void testGetLibraryListByJackson2Xml() throws Exception {
        LibrariesForJackson2Xml libraries = calilApiService.getLibraryListByJackson2Xml("東京都");
        assertThat(libraries).isNotNull();
        System.out.println(libraries);
    }
  • testGetLibraryListByJackson2Xml メソッドを追加します。

■その2

    @Test
    public void testGetLibraryListByJackson2Xml() throws Exception {
        LibrariesForJackson2Xml libraries = calilApiService.getLibraryListByJackson2Xml("東京都");
        assertThat(libraries).isNotNull();
        System.out.println(libraries.toString());
        assertThat(libraries.getLibraryList()).isNotNull();
        libraries.getLibraryList().stream()
                .forEach(s -> System.out.println(s.toString()));
    }
  • libraries.getLibraryList() のデータを1件1行で出力されるようにします。

■その3

    @Test
    public void testGetLibraryListByJackson2Xml() throws Exception {
        LibrariesForJackson2Xml libraries = calilApiService.getLibraryListByJackson2Xml("東京都");
        assertThat(libraries).isNotNull();
        // systemname が "国立国会図書館" のデータがあるかチェックする
        assertThat(libraries.getLibraryList())
                .extracting("systemname")
                .contains("国立国会図書館");
    }

履歴

2015/08/30
初版発行。