こまぶろ

技術のこととか仕事のこととか。

StreamとOptionalに共通して存在するメソッド(その2):of / ofNullable / empty

StreamとOptionalに共通して存在するメソッドの第二弾。 第一弾はこちら。

ky-yk-d.hatenablog.com

前回の記事では、filter()について書いた。今回は、of()ofNullable()について書……こうと思ったら実はempty()も共通して存在するメソッドだったことに気づいたので、これも併せて3つのメソッドについて書く。

ファクトリメソッド: of()ofNullableempty()

メソッド Stream Optional
of() static Stream of​(T t) public static Optional of​(T value))
ofNullable() static Stream ofNullable​(T t) public static Optional ofNullable​(T value)
empty() static Stream empty() public static Optional empty()

of()ofNullable()empty()は、StreamとOptionalのいずれにおいてもstaticのファクトリメソッドだ。Streamはクラスではなくインタフェースだが、Java 8からはインタフェースにstaticメソッドが書けるようになっているため*1、ファクトリメソッドを定義することができる。実際には、下記のように定義されている。

public static<T> Stream<T> of(T t) {
    return StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
}

public static<T> Stream<T> ofNullable(T t) {
    return t == null ? Stream.empty()
                     : StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
}

public static<T> Stream<T> empty() {
    return StreamSupport.stream(Spliterators.<T>emptySpliterator(), false);
}

対して、Optionalのそれぞれのメソッドの定義は下記の通りだ。

public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}

public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}

public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}

こうして並べてみると、StreamとOptionalのそれぞれにおいて、of()ofNullable()という2つのメソッドの関係が同じであることがよくわかる。of()が引数で受け取った何らかの値をそのままStreamないしOptionalに包んで「返そうとする」のに対して、ofNullable()は引数がnullだった場合は空のインスタンス(それぞれのempty()メソッドで取得する)を返し、nullでない場合はof()と同じ結果を返すようになっている。空のインスタンスについては、その挙動を含めて次回に回す。

of()にnullを渡した場合の挙動の差異

それでは、of()の引数にnullを渡した場合の挙動はどうなるのか。これはStreamとOptionalで異なっており、Streamのof()が、単に(nullが実体を持ったオブジェクトであるかのような表現には語弊があるが)「null 1つ」を要素とするStreamを返すのに対し、Optionalのof()NullPointerExceptionをスローする*2。下記のコードで実際に動作を確認できる。

/* JUnit5を利用 */
@Test
void Streamのofはnullを含むStreamを返す() {
    List<Object> list = Stream.of((Object)null).collect(Collectors.toList());
    assertEquals(1, list.size()); // 要素数は1
    assertNull(list.get(0)); // その要素はnull
}
@Test
void Optionalのofは例外をスローする() {
    final String helloWorld = "Hello World";
    Optional<Object> opt = Optional.of(helloWorld);
    Exception exception = null; // スローされたら格納しておく
    try {
        opt = Optional.of(null);
        fail("例外がスローされるので到達したらいけないコード");
    } catch (Exception e) {
        exception = e;
        assertEquals(NullPointerException.class, e.getClass());
    }
    assertEquals(helloWorld, opt.get()); // 当初の値のままであるはず
    assertNotNull(exception); // 例外がスローされていることを確認
}

その他の違い

以上のように、of()にnullを渡した際の挙動はStreamとOptionalとで異なっている。その他に、異なる点としては、

  • Optionalのof()には(Optionalの性質上)単一の引数のものしか存在しないのに対して、Streamのof()にはof​(T... values)という可変長引数のものも存在している。
  • これら6つのメソッドのうち、StreamのofNullable()だけは導入されたバージョンがJava 9であり、一世代新しい。

という点が挙げられる。

感想と次回予告

じっくりと仕様と実装を見てみると、普段いかに雰囲気で使ってしまっているかに気づくことができた。とはいえ、ファクトリはファクトリでしかないので、それぞれの生成の仕方にどのような意味があるのかは、生成が利用される場面とセットでなければわかりにくい。次の記事では、map()flatMap()を紹介しながら、今回の記事で扱ったファクトリをどのように利用するかにも言及したい。

*1:また、インスタンスメソッドについてはdefaultキーワードを用いてデフォルト実装を定義することもできるようになっている。

*2:スローするのはof()の中で呼び出しているOptionalのコンストラクタ、更に言えばそこで呼び出しているObjects.requireNonNull()だ。

StreamとOptionalに共通して存在するメソッド:filter(Predicate<? super T> predicate)

Java 8 で導入されたStreamとOptional。業歴は浅いのに古いJavaで学んでしまったので、いざ使える状況になったにもかかわらずいまいち使い方がよくわかっておらず、書籍やWeb上の資料で知識を補っている最中だ。

調べているうちに、Stream(インタフェース)とOptional(クラス)との間には、(名称が)共通のメソッドがいくつか存在することに気が付いた。APIドキュメントを見比べると、以下のものがそれに当たる。

  • filter()
  • flatMap()
  • map()
  • of()
  • ofNullable()

StreamとOptionalについての勉強の手始めに、これらについて調べてみることにした。これらのうち、filter()of()ofNullable()は引数の数と型も一致(=シグネチャが一致)している。今回は、このうちのfilter()について、StreamのそれとOptionalのそれとの比較を試みる。

述語を満たす要素を残す

filter()の引数は、いずれもPredicate<? super T> predicateだ。Predicateは関数型インタフェースで、ひとつの入力に対してbooleanの判定結果を返すtest()メソッドを持つ。具体的な事例では、下記のような形で用いられる。

number.filter( n -> n % 2 == 0) // number は Optional or Stream

上のコードでは、numberの要素に対して、それが偶数であるかを判定している。判定結果がfalseとなる場合、Optionalでは判定結果がfalseであれば空のOptilnal(Optional.empty()の返り値)が返却されるのに対し、Streamでは当該要素を除いた要素で新たなStreamが返却される。

いずれにおいても、判定結果がfalseになる要素を除外する(trueになる要素のみを残す)という意味合いは共通していることがわかる。OptionalもStreamも中に要素を持つ「入れ物」としてイメージできるが、その要素をなんらかの基準によって選別するという機能をfilter()は持っている。

OptionalとStreamの違い

ただし、両者の間には当然ながら違いもある。「入れ物」としてのOptionalとStreamの違いは、Optionalは要素を1つしか持たないのに対し、Streamは0から多数の要素を持つことができる、という「要素の数」の違いとして整理しても間違いではないと思う。しかし、filter()の挙動からすると、両者の間には要素の数以上の違いがあることも見えてくる。

Optionalは、1つの要素を持つというよりは、1つを収めるスペースを持っていると表現すべきだろう。判定結果がfalseの場合のfilter()の返り値は、空のOptional、すなわち「1つ分のスペースが空席の」Optionalである。そこには、空席にはなっているものの、要素1つ分のスペースが確かに残っている。これに対し、Streamのfilter()は要素の数が単純に減った新しいStreamが返される。filter()を受ける前に存在していた要素の痕跡は残らない。

次回予告

今回は、filter()について調べた。言及したOptionalとStreamの違いは、その他の共通のメソッドにも反映されているはずなので、これからまた調査したい。

関数型プログラミング未経験者がREPLでClojureに触ってみた

下記の記事を読んでいたら、Clojureというプログラミング言語の名前がちょこちょこと登場しているのが目に入り、少し調べてみることにした。

techracho.bpsinc.jp

Clojureとは

Clojureとはどんな言語なのだろうか。Wikipediaをみてみる。

Clojure (発音は/'klouʒər/[2], クロージャー)はプログラミング言語であり、LISP系の言語の方言の一つである。関数型プログラミングのプログラミングスタイルでのインタラクティブな開発を支援し、マルチスレッドプログラムの開発を容易化する汎用言語である。

LISP系の言語の方言」とある。LISPというのは名前だけ聞いたことがある程度だ。プログラミング言語で「方言」というと、SQL方言が真っ先に思い浮かぶが、LISPというのも標準語と複数の方言が存在する言語なのだろうか、とLISPについて調べ始める。なるほどLISPというのは歴史のある言語で、計算機プログラムの構造と解釈』(SICPという有名な教科書でも「Scheme」というLISP方言の言語が使われているようだ。

SICPは、プログラマ、というか計算機科学に関わるならば読んでおきたい書籍という扱いがされているようだ。日本語版のPDFでも700ページを超える重厚なもので、読み切るのは大変だろうが、とても勉強になりそうというのもまた確か。感想記事にはたくさんのはてブが付いている。いつか読んでみたい。

kinokoru.jp

と、やっているとキリがないので、本題に戻って、Clojureを少し触ってみることに。しっかり環境構築をしようと思うと最初で挫折するので、手軽に試してみようと考え、公式ドキュメントで紹介されているREPLによる入門をやってみることにした。

Clojureの対話型実行環境「REPL」

様々な言語にREPLが存在するが、ClojureにもREPLがある。これは、ClojureのCLIツールに同梱されているので、簡単に使い始めることができる。Mac環境では、ClojureをHomebrewでインストールすれば使えるようになる。

$ brew install clojure
$ clj
Clojure 1.10.0 
user=> 

これで、REPLが立ち上がり、Clojureのプログラミングがでできるようになっている。下記の公式ドキュメントにしたがって、いくつか操作を試してみる。

パッとみて、括弧が激しく使われるなぁ、と思っていたら、REPLでは)を入力したときに対応する(がハイライトされるようだ。これで少しは助かる。

f:id:ky_yk_d:20190126160330g:plain

Clojureでは+も関数

さて、手始めに足し算か、と思ったら、足し算からしてまず違う。演算子は前置記法で書く。

user=> (+ 2 3)
5

これは、関数と同じ書き方になるということでもある。というか、Clojureでは+は立派な関数らしい。Javaがメインのプログラマにとって、演算子は通常のメソッドとは違う特別な存在だが、このClojureでは演算子も関数として定義されている。C++では演算子オーバーロードできるし、プログラミング言語によって、同じような機能を持っているものの位置付けが違うのは面白いなと思う。

関数を定義するdefnは「マクロ」

次は関数の定義。ここでは階乗の関数を定義している。

user=> (defn factorial [n]
(if (= n 0)
  1
  (* n (factorial (dec n)))))
#'user/factorial
user=> (factorial 10)
3628800

defnというのは関数ではなく、「マクロ」というものらしい。C言語から入っているので、マクロというと、#defineなのだけど、LISPのマクロは一味違うらしい。なるほどわからん

ifは評価値を持つ「特殊形式」

また、面白かったものとして、ifがある。C言語Javaで制御構造の一つをなしているifが「文」を構成する(評価された値を持たない)のに対し、Clojureifは関数のように評価された値を持つ「式」ということになる*1Clojureifは厳密には関数ではなく、「特殊形式」というものに当たるらしい。

Clojureifは、(if test then else)という書き方をして、testの評価値がnilfalseでない場合はthenを評価し、nilfalseの場合はelseを評価するというものになっている。関数的な言い方をすると、引数を3つとるわけだが、これはC言語Javaの条件演算子Hoge ? Fuga : Piyoに似ている。

CとJavaを学んだときは、「なんだよこれわかりにくいな、なんでこんなんあるんだ」と感じ、最近は、「これ使うと行数少なく書ける」くらいに感じていた条件演算子だったが、関数型の世界でのifがこのような形になるのはなるほど自然で、にわかに条件演算子が由緒正しいものに思えてきた。

APIドキュメントを表示するマクロdoc

また、REPLには、docという便利な機能がある。(doc name)とREPLで入力すると、nameで指定したもののAPIドキュメントが表示される。いくつか例を示す。

f:id:ky_yk_d:20190126225447p:plain
「+」の説明も存在する

f:id:ky_yk_d:20190126225520p:plain
「doc」自体はマクロ

この機能があれば、REPLを使っていて関数などの使い方がわからなくなったときも、わざわざブラウザでドキュメントを見る必要がない。とても便利な機能だと感じた。

Clojureにおける「REPL駆動開発」

このdocをはじめとして、ClojureのREPLは単なる「入門用の遊び場」に止まらない機能を有している。そのためか、Clojureにおいては「REPL駆動開発」が書籍でも言及されているほどらしい。

感想

プログラミング言語は、C言語Java(Stream API不使用)と学び始めて、それからある程度書いたことがあるのもPythonRubyJavaScript(TypeScript)あたりで、関数型言語は全く経験がない。Scala去年の8月に入門的レベルをほんの少しだけ触ってそのままになっていて、本格的に関数型らしいところまでたどり着かなかったので、今回は初っ端から関数型らしさを感じられて面白かった。

現在、約1年振りにJavaを仕事で書くことになり、前回は使わなかったStream APIも使い始めている。先日のJJUGのイベントで、GoFデザインパターンの「振る舞いに関するパターン」をラムダ式で置き換えるというライブコーディングを見たこともあり、関数型プログラミングに対する関心が高まっている。オブジェクト指向もよくわかっていないのだけれど、せっかくStream APIを使えるのだから、関数型の考え方も取り入れながらプログラムが書けたらいいなと考えている。

計算機プログラムの構造と解釈 第2版

計算機プログラムの構造と解釈 第2版

プログラミングClojure 第2版

プログラミングClojure 第2版

*1:触ったことがある言語では、RubyScalaのifも「式」だった。

Java 9 以降の非推奨モジュールに依存したプログラムを動作させる:OpenAPI Generatorで生成したSpringスタブサーバが動作しない場合の対処法

前回の記事で、OpenAPI GeneratorでSpringのスタブサーバのソースコードを生成する方法を紹介した。

ky-yk-d.hatenablog.com

上の記事に記載した手順を手元のMacBook Proで実行したとき、XmlModelPluginに関わるエラーが発生してSpring Bootアプリケーションとして動作しなかった。別の端末(iMac)では正常に動作したので前回の記事では言及しなかったが、調査してみたところわかったことが多かったので、記事として公開することにした。

あらかじめ書いておくが、今回のエラーの根本原因は後述の通り自分がJavaのバージョンに無頓着であったことであり、OpenAPI Generatorの問題ではない。下記の内容は、OpenAPI Generator固有の問題を扱っているものではなく、Javaのバージョンの差異によって生じるエラーのケーススタディ的なものだと思ってもらえばいい。

概要

項目 内容
事象 生成したSpringのスタブサーバが特定環境で動作しない
原因 Java 11 で非推奨のモジュールが解決されなかったため
対応 pom.xmldependenciesJAXB APIを追加する

発生した事象

OpenAPI Genaratorでソースコードを生成したSpringのスタブサーバが動作しなかった。具体的には、下記のような手順でスタブサーバを起動しようとすると、エラーが発生してしまっていた。

実行手順

下記のコマンドを実行し、OpenAPI Generatorによるスタブサーバのソースコードを生成する。

openapi-generator generate
    -i docs/openapi.yaml # 生成元ファイル
    -o generated-sources/spring_stub # 出力先ディレクトリ
    -g spring
    --additional-properties returnSuccessCode=true

STSを開き、Open Projects from File System ...を選択。生成されたソースコードを読み込ませる。

f:id:ky_yk_d:20190113103609p:plain
ファイルシステムからプロジェクトを開く

STS上のBoot dashboardから、読み込んだプロジェクトを選択して起動する。

f:id:ky_yk_d:20190113105011p:plain
Boot dashboard から起動する

発生するエラー

下記のエラーメッセージがコンソールに表示され、アプリケーションの起動に失敗する。

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2019-01-12 17:17:36.297 ERROR 6665 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'xmlModelPlugin': Lookup method resolution failed; nested exception is java.lang.IllegalStateException: Failed to introspect Class [springfox.documentation.schema.XmlModelPlugin] from ClassLoader [jdk.internal.loader.ClassLoaders$AppClassLoader@512ddf17]
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors(AutowiredAnnotationBeanPostProcessor.java:262) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.determineConstructorsFromBeanPostProcessors(AbstractAutowireCapableBeanFactory.java:1198) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1123) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:541) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:501) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:317) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:315) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:760) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:869) ~[spring-context-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550) ~[spring-context-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:140) ~[spring-boot-2.0.1.RELEASE.jar:2.0.1.RELEASE]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:759) ~[spring-boot-2.0.1.RELEASE.jar:2.0.1.RELEASE]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:395) ~[spring-boot-2.0.1.RELEASE.jar:2.0.1.RELEASE]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:327) ~[spring-boot-2.0.1.RELEASE.jar:2.0.1.RELEASE]
    at org.openapitools.OpenAPI2SpringBoot.main(OpenAPI2SpringBoot.java:24) ~[classes/:na]
Caused by: java.lang.IllegalStateException: Failed to introspect Class [springfox.documentation.schema.XmlModelPlugin] from ClassLoader [jdk.internal.loader.ClassLoaders$AppClassLoader@512ddf17]
    at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:659) ~[spring-core-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:556) ~[spring-core-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:541) ~[spring-core-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors(AutowiredAnnotationBeanPostProcessor.java:245) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    ... 16 common frames omitted
Caused by: java.lang.NoClassDefFoundError: javax/xml/bind/annotation/XmlType
    at java.base/java.lang.Class.getDeclaredMethods0(Native Method) ~[na:na]
    at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3167) ~[na:na]
    at java.base/java.lang.Class.getDeclaredMethods(Class.java:2310) ~[na:na]
    at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:641) ~[spring-core-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    ... 19 common frames omitted
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.annotation.XmlType
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582) ~[na:na]
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178) ~[na:na]
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521) ~[na:na]
    ... 23 common frames omitted

発生原因

OpenAPI Generatorが生成したソースコードが想定するJavaのバージョンが8であるのに対し、MacBook ProにインストールしているJavaのバージョンが 11 であり、Java 9 以降で非推奨となっているjavax.xml.bindのモジュールが解決されなかったため。

Java 9 以降で非推奨になったモジュール群

(エラーの発生しなかった)iMacにインストールしてあったのがJava 8 であったのに対し、MacBook Proでは、以前遊びでJava 11 をインストールしてあった。下記の記事に拠れば、Java 9 以降ではいくつかのモジュールが非推奨となっており、デフォルトで解決されずに例外が発生してしまう。

javax.xml.bind

今回の場合、エラーの原因となっていたのは、javax.xml.bindというモジュールだった。このモジュールも、Java 9 以降で非推奨となっているものだ。エラーメッセージの末尾をみると、確かにClassNotFoundExceptionが発生していることがわかる。

Caused by: java.lang.ClassNotFoundException: javax.xml.bind.annotation.XmlType
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582) ~[na:na]
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178) ~[na:na]
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521) ~[na:na]
    ... 23 common frames omitted

改めて、エラーメッセージをみてみると、以下のように、XmlModelPluginクラスでIllegalStateExceptionが発生している。つまり、このXmlModelPluiginが、解決されないjavax.xml.bindモジュールに依存していることにより、今回のエラーは発生していたようだ。

java.lang.IllegalStateException: Failed to introspect Class [springfox.documentation.schema.XmlModelPlugin] from ClassLoader [jdk.internal.loader.ClassLoaders$AppClassLoader@512ddf17]

OpenAPI GeneratorとSpringFox

なお、上記のXmlModelPluginSpringFoxの一部であるが、OpenAPI GeneratorのGitHubリポジトリの文書を確認してみると、生成されるSpringのスタブサーバはデフォルトでSpringFoxを利用しているという記載が確かに存在する。

library
    library template (sub-template) to use (Default: spring-boot)
        spring-boot - Spring-boot Server application using the SpringFox integration.
        spring-mvc - Spring-MVC Server application using the SpringFox integration.
        spring-cloud - Spring-Cloud-Feign client with Spring-Boot auto-configured settings.

エラー解消のための対応

Java 8 に戻す……のではなく、前掲の記事にあるように、明示的に「Maven Central 上のアーティファクトを利用するよう依存ライブラリを追加」する。具体的には、下記をpom.xmldependenciesに追加すればよい。

<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
</dependency>

以上の記述を追加してから、Boot dashboardから再起動すると、エラーが発生することなく正常に動作するようになった。ブラウザからAPIにアクセスすると、下記のように表示される。

f:id:ky_yk_d:20190112172255p:plain
http://localhost:8080/detailsにブラウザでアクセスした場合

感想

Javaのバージョンの差異に因って生じるエラーということは思いつかなかった。そもそも自分のマシンにインストールされているJavaのバージョンをあまり意識できていなかった。自宅ではJavaを書くことがほとんどないとはいえ、Javaで飯を食っている人間としては猛省すべき事案だった。「iMacなら動くし、まぁいっか」で済ませてしまわずに、原因を調査したことで左記のことに気づくことができたのが救いか。

今回は、対応として依存モジュールを明示するという方法を書いたが、Java 11 で動作するソースコードを生成できればそれに越したことはない。この点については、そのような変更がOpenAPI Generatorで可能なのかどうかをまだ理解できていないので、また何かわかったら記事にしたいと思う。

OpenAPI GeneratorでRESTful APIの定義書から色々自動生成する

APIの定義を書く:Excel仕様書はもういやだ

RESTful APIを提供するサーバと、そのAPIを利用するクライアント(たとえばSPA)とを並行で開発しようとするとき、まずAPIを定義して、それに基づいてサーバ/クライアント双方の実装を進めようと考えるのは自然だと思う。

そうと決まれば、「API仕様書_20190110.xlsx」と題するファイルを新規作成し、シート別にリソース毎の定義を書き始め・・・てはいけない。せっかくAPIを定義したドキュメントを作成するなら、するのなら、ソースコードの自動生成などの恩恵も受けたい。受けられるはずだ。

少しググってみる。どうやらSwaggerというものを使えばいいらしい。Swaggerに興味を持ったタイミングで、ちょうど書店に平積みになっていた『WEB+DB PRESS Vol.108』の表紙が目に入った。そこには、「スキーマ駆動Web API開発 OpenAPI/GraphQLで仕様からコードもテストも作成」の文字。

WEB+DB PRESS Vol.108

WEB+DB PRESS Vol.108

  • 作者: 中野暁人,山本浩平,大和田純,曽根壮大,ZOZOTOWNリプレースチーム,権守健嗣,茨木暢仁,松井菜穂子,新多真琴,laiso,豊田啓介,藤原俊一郎,牧大輔,向井咲人,大島一将,上川慶,末永恭正,久保田祐史,星北斗,池田拓司,竹馬光太郎,粕谷大輔,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2018/12/22
  • メディア: 単行本
  • この商品を含むブログを見る

この特集がよかった。この特集では、そもそもOpenAPIがどのような需要に応ずるものなのかから、(この記事では言及しないが)API定義に基づいて自動テストを実行する方法まで、サンプルも用いながら説明しており、入門には最適だった。サンプルコードは下記のGitHubリポジトリで公開されている。

github.com

この特集をきっかけとして、OpenAPI関連のツールを少し使ってみたので、まとめておく。

OAS準拠のAPI定義を書く

「OpenAPI Specification」とは

OpenAPI Specification(OAS)は、RESTful APIを定義するための標準仕様だ。OASにしたがって記述されたJSONあるいはYAMLファイルからは、HTMLドキュメント、スタブサーバやAPIクライアントのソースコードなどを自動生成することができる。テキストファイルなので、Gitなどでバージョン管理をすればdiffを確認することも容易になる。

OASドキュメンテーション

github.com

OASのドキュメントは、上掲のGitHubリポジトリのほか、SwaggerのWebサイトにもまとまったものが存在している。元々、現在のOASのベースとなる仕様はSwaggerという名称で、標準化団体であるOpenAPI Initiativeに所有権が移り、2016年に名称がOASに変更されてからも、開発されてきた周辺ツールの数々はSwagger 〇〇という名称のまま存続している。

インストールなしで使える「Swagger Editor」

Swaggerの周辺ツールのうち、一番わかりやすいのが、Swagger Editorだろう。複数のツールを統合したもので、インストール不要で利用できるオンライン版がすぐ試せる*1。画面の左側にOASに準拠した内容を入力すると右側にリアルタイムでHTMLドキュメントのプレビューが表示されるほか、記述内容に即したスタブサーバやAPIクライアントのソースコードを、上部のメニューから生成することができる。

f:id:ky_yk_d:20190113233854p:plain
Swagger Editor(オンライン版)

VS Code拡張「Swagger Viewer」

簡単な編集であれば、オンライン版のSwagger Editorで事足りるが、実際のアプリケーションのAPIを定義するのであれば普段使っているエディタで編集できた方が都合がいい。そのような需要に対しては、先述の特集記事の中でも紹介されている、Visual Studio Code拡張機能の「Swagger Viewer」が優秀だ。OASの構文エラーを表示してくれるほか、Swagger Editorと同じようにHTMLドキュメントのプレビュー表示にも対応している。

marketplace.visualstudio.com

OpenApi Generatorを使ってみる

Swagger Codegenからフォークした「OpenAPI Generator」

OAS準拠のAPI定義からソースコードやHTMLドキュメントを生成するのは、Swaggerの名を冠するツールの中では「Swagger Codegen」の役割だ。Swagger Editorの画面からソースコードを生成するときにも、このSwagger Codegenが機能している。

Swagger Codegenを利用してもいいのだが、今回は冒頭に掲げた特集で紹介されている「OpenAPI Generator」を利用してみることにする。Swagger Codegenが、かつて仕様としてのSwagger(現在のOAS)を所有していたSmartBear社メンバーを中心に開発されているのに対し、Swagger CodegenからフォークされたOpenAPI Generatorはコミュニティによって開発されている。この辺りの事情については、OpenAPI Generaterのコミッターであり、特集にも寄稿されている @NAKANO_Akihito さんのブログ記事に書かれている。

ackintosh.github.io

OpenAPI Generatorをインストールする

github.com

OpenAPI GeneratorはJava製ツールで、Maven Centralから取得することができる。インストールの方法は様々用意されている。今回は、NPMパッケージ版を利用してみることにした。クライアントの開発でNPMを利用することが多いから、有力な選択肢になるのではないだろうか*2

github.com

インストールは通常のNPMパッケージと同じようにnpm installコマンドを実行すればいい。グローバルインストールしてもいいが、クライアント側のアプリケーションにローカルインストールしておくと、package.jsonのscriptが使えるので便利だと思う。

OpenAPI Generatorを使った自動生成を試す

HTMLドキュメントを生成する

VS CodeのSwagger Viewerでリアルタイムにプレビューすることができるので、YAMLファイルを編集している際はそちらを見ればいい。しかし、APIの定義を見るのにわざわざVS Codeを開くのは面倒だから、手軽に見られる静的なHTMLドキュメントが欲しくなる場面もある。

OASの定義ファイルから、OpenAPI GeneratorでHTMLドキュメントを作成することができる。generateコマンドで、-gオプションの引数をhtmlとすればいい。

openapi-generator generate
     -i docs/openapi.yaml # 定義ファイルの指定
     -o docs/html # 出力先ディレクトリの指定
     -g html 

定義ファイルは長大になるのであえて記載しないが、動作を試したい場合はSwagger Editorにアクセスした際にデフォルトで表示される、Swagger Petstoreをそのままコピーしてみればいい。自分で書いてみたYAMLファイルを元に生成したドキュメントは、以下のようなものになる。

f:id:ky_yk_d:20190113214539p:plain
生成されたHTMLドキュメント

APIクライアントを生成する

APIクライアントの作成にも対応している。HTMLドキュメントを作成するのと同様、generateコマンドを用いる。-gオプションの引数に、生成したい言語/フレームワークを指定してあげればいい。選択できる言語とフレームワークの一覧は、GitHubのREADMEに記載にある通り、多岐にわたっている。特集ではJavaScriptソースコードを作成していたが、下の例では、Angular用のクライアントを生成している。このクライアントの使い方については、後日試してみようと思う。

openapi-generator generate
    -i docs/openapi.yaml
    -o generated-sources/client
    -g typescript-angular
    --additional-properties="ngVersion=7.2.0"

スタブサーバーを生成する

APIの定義中には、exampleとして各データ型の値の例示を含めることができる。この各データ型の値の例を元に組み上げられるレスポンスボディのサンプルは、HTMLドキュメント内で表示されるほか、OpenAPI Generatorで生成したスタブサーバの返却する値としても利用できる。

スタブサーバを用意すれば、クライアントの開発はAPIサーバの実装を待たずに実際の動作を確認しながら進められる。スタブサーバも、OpenAPI Generatorのgenerateコマンドで生成することができる。下記では、Springのソースコードを生成している。

openapi-generator generate
     -i docs/openapi.yaml
     -o ../spring_stub
     -g spring
     --additional-properties returnSuccessCode=true

生成したスタブサーバを動作させるのも難しくはない。STSを開いて、ファイルシステムからプロジェクトを開き、Boot dashboardから起動するだけだ。

f:id:ky_yk_d:20190113103609p:plain
ファイルシステムからプロジェクトを開く

f:id:ky_yk_d:20190113105011p:plain
Boot dashboard から起動する

コンソールで起動が確認できたら、スタブサーバが利用できるようになっている。試しに、ブラウザで今回定義したURIのひとつであるlocalhost:8080/detailsにアクセスすると、下記のようにJSONが取得でき、スタブサーバが動作していることが確認できる。

f:id:ky_yk_d:20190112172255p:plain
5000兆円欲しい

感想

設計書というとExcelという環境で仕事をしてきているので、「YAMLからコードを自動生成する」なんてことはどこか別の世界の営みだと感じてしまう部分があった。また、アプリのソースコードを書くのは楽しいと感じていて、勉強するのもそんなに心理的ハードルがないのに対して、XMLYAMLといった設定ファイルの記述は面白くなさそうなイメージがあり、勉強する際も避けて通りがちだった。

JSONは書く機会がそこそこにあるので、さすがにもう 「気味の悪い拡張子」 だとは思わなくなっているが、YAMLは書いたことがなかったので、いい機会になった。経験を積んでいけば、自動化のメリットも感じられるようになるだろうし、単純に見る機会が増えれば苦手意識もなくなると思う。これからも色々試してみたい。

2019.02.03追記

このブログを書いたあとに公開された資料がとても充実したものだったので、紹介する。この資料は、特集記事の著者の一人である中野さんが勉強会での発表で用いたものだ。本記事もスライドの中で少し登場させていただいている。この場でお礼申しあげたい。

*1:インストール版もある。

*2:とはいえ、あくまでラッパーでしかなく、Java製ツールであることには変わりはないので、JVMが不要になるわけではない。