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

astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

Java8になってから開発者は色々と楽になった、のだろうか?Spring BootでWebアプリを作って検証してみた

どうもえいやです。今回の技術ブログを担当します。

前回はGebの話をしました。皆もうブラウザテストは自動化出来たかな。

今回は、前回と変わって当社のWebアプリケーションのバックエンド開発言語となっているJavaの話です。

ラムダ式使ってる?

Java8がリリースされてずいぶんと経ちますが、皆さんの現場ではJava8の力はちゃんと発揮されているでしょうか。

先日、同僚が出かけたJavaのイベントでは、講演者が参加者に向けて、「Lambda式を使っている人は手を上げてみてください」と求めて見たところ、ほとんど手が上がらなかったそうです。

当社では、エンジニアは全員Scalaが書けるということになっていますので、Javaの案件でも特に違和感(Scalaのそれに比べ、少し(?)不便なのは置いといて)なく使えているんですがねぇ。。。

とまれ、Java8では、それまでのJava7から、言語仕様の面などで色々とちゃんと使えば便利になるはずのものがたくさん追加されています。

今回の内容

今回は、Java8で追加された言語仕様を極力多用する形で、ある程度業務レベルの仕様を想定したサンプル・Webアプリケーションを0からスクラッチで書いてみるということをしてみました。

目的は、Java8の主に言語仕様とその仕様を用いることを想定した新規のライブラリおよび機能を使いこなすことで、Web開発での利用において(RubyOnRailsなどに比べ)生産性が悪いと言われているJavaの問題を少しくらい払拭できるのかを検証してみることです。

なお、当社では普段のWeb開発のフレームワークには、自社製のAsta4Dを使用していますが、今回は使用者が多いと思われるSpring Bootを用いました。

結論から言うと、スクラッチ開発が楽になるものではないな、ということでした。完成してませんもん。

話はそれまでなのですが、続きを読みたい方は以下続きます。

せっかくなので、今回作る際にどんな感じでLambda式などの仕組みが出てきたのかとかも解説します。

Java8だからといって、何が楽にならないのか

比較対象として、GroovyのRails実装であるGrailsについて見てみると、Grailsで2日で出来たことが、今回の検証では(真面目に実装をやり始めて)4日経ってもまだ半分しか完成していません。

比較対象にGrailsを選んだ理由は、仕組みは大体わかっているが普段の仕事ではあまり使ってない、というWebフレームワークだからです。

Grailsは実は今回使用したSprinng Bootを土台に、上にRailsの実装を施して楽に開発出来るようにしたようなものなので、最低限使える程度のテンプレートやORMの仕組みなどを自分で追加しなければならないJava+Spring Bootで想定するスクラッチ開発とはスタート地点が異なります。

そういう意味では比較対象としてはふさわしくはないのですが、改めてJavaのWeb開発において一般的に言われている生産性を阻害する点を実感したところです。

Java+Spring Bootでスクラッチ開発をやろうとなったとき、実際のところ5割以上の時間を費やすのがこの開発を開始するための土台作りです。

Railsなどではこの土台が既に出来ており、しかも大体の場合、自分で作った土台より遥かに出来が良い状態から始まるのです。

土台作りに慣れていて、ある程度分かっていたとしても、以前使ったことのあるものがバージョンアップされていたりすると、妙なところでハマったりしてなかなか険しい道のりです。

今回は普段の開発でも似た仕組みを持っていて、ある程度手慣れている部分でも大ハマリしてしまいました。

こんななので、秘伝のタレのような、一度に大きな変更や更新をしてはいけないFW設定を持つチームも多いでしょう。

こうした部分は、もはや言語の問題ではないので(JavaでGrailsを使うことも出来る)今回の検証の仕方が悪かったかなという気がしなくもないです。

と言っても、僕の想像している範囲では、Javaの開発って保守と改修以外ではいっつもココから始めてるよなぁという印象です。

この辺が楽にならないと新規の開発は永遠に楽にならないと思います。

本題の検証用Webアプリケーション

さて、今回検証用に作った(作りかけですが)Webアプリケーションはさらっとした感じでは以下のようなものです。

f:id:astamuse:20161115200417p:plain

使用する技術、アーキテクチャとしては以下となります。

f:id:astamuse:20161115200403p:plain

Spring FWには、Spring Bootはじめ色々と含まれていますが、まとめて箱にいれました。あとでソース置いてあるところ示しますので、build.gradleをみて確認しておいてください。

ビルドにはGradleを使いました。Spring Bootの起動には、Grettyプラグインを使用します。

年齢の制限などを加えている部分は、対象のコンテンツかどうかAOPで検出します。この際にはAspectJ形式のアノテーションを利用します。

設定に関しては、Javaコードで書くことがメインなのでXMLを用いずにJavaで設定を書くことにしています。

その他HTMLテンプレートとしてthymeleaf、フロントエンドの実装のためにBootstrapjQueryを使用しました。

フロントで複雑なことはしないので、このあたりのことには触れません。とはいえ、(サーバサイドのエンジニアなりに)ちゃんとやった感がでるくらいには頑張りました。

んで、見た目はほとんどBootstrap頼りで以下みたいな感じにします。

f:id:astamuse:20161115200427p:plain

(作りかけの)コード

さて、どんな感じに設計しているとかは置いといて、作りかけですが、コードをGithubに上げておきます。試行錯誤の後があまりに汚ないので、リポジトリにログは残していない形です。

github.com

作りかけではありますが、一応起動できる状態ですので、ためしに動かしたければどうぞ、つってもアカウントの登録の時点で動いてないんだけどね。

この記事までに完成を間に合わせたかったのですが、土台をイチから作っていると、色々なところでハマるので間に合っていません。ただ、記事には出来るように、普段の作り方とは逆で、実装が面倒そう、Java8の力が発揮できそうな部分から作って行ったので、コードとしての完成度はそれなりにあります。

今回は完成させるのが目的ではなかったので、検証結果は「やっぱり大変なことに変わりない」ということで良いと思います。

では、この作りかけの-一応動くところは意図通りに動いている-コードをもとに、Lambda式などのJava8になってから追加されたコードがどのように現れてくるのか、使った意図の説明を交えて見ていきましょう。

Lambda式の使用ポイント その1 その場その場でコードを快適に書く設計

一つのファイルのコードにおいて一番Lambda式が目立つのは、

jp.eiya.aya.web.util.security.PermissionManager

でしょう。以下のような感じです。

public class PermissionManager extends AbstractPermissionManager {
    public static final class Permissions{
        public static final String NOT_LOGIN_SESSION = "notLoginSession";
        public static final String HAS_ACTIVE_LOGIN_SESSION = "hasActiveLoginSession";
        public static final String HAS_PAID_TICKET = "hasPaidTicket_computed";
        public static final String ACCOUNT_INFO_IS_COMPLETED = "accountInfoIsCompleted";
        public static final String OVER_18_AND_MORE = "over18AndMore";
        public static final String UNDER_18_LESS = "under18Less";
        public static final String OVER_20_AND_MORE = "over20AndMore";
    }

    @Named(Permissions.HAS_ACTIVE_LOGIN_SESSION)
    public Permission hasActiveLoginSession() {
        return () -> loginService.getLoginUser().isPresent();
    }

    @Named(Permissions.NOT_LOGIN_SESSION)
    public Permission notLoginSession()  { return ()-> !hasActiveLoginSession().isGranted(); }

    @Named(Permissions.HAS_PAID_TICKET)
    public Permission hasPaidTicket() {
        return () ->
            checkIfPresent(
                loginService.getLoginUser(),
                (loginUser) ->
                    paymentService.getPaymentStatuses(loginUser).stream()
                    .filter(PaymentStatus::hasPaidTicket_computed)
                    .findAny()
                    .isPresent()
            );
    }

    @Named(Permissions.ACCOUNT_INFO_IS_COMPLETED)
    public Permission accountInfoIsCompleted() {
        return () ->
            checkIfPresent(
                loginService.getLoginUser(),
                (loginUser)->accountInfoService.getAccountInfo(loginUser).isCompleted_computed()
            );
    }

    @Named(Permissions.OVER_18_AND_MORE)
    public Permission over18AndMore() {
        return () ->
            checkIfPresent(
                loginService.getLoginUser(),
                (loginUser)->
                    checkIfPresent(accountInfoService.getAccountInfo(loginUser).getAge_computed(),
                    (age) -> age >= 18
                )
            );
    }

    @Named(Permissions.UNDER_18_LESS)
    public Permission under18Less() {
        return () ->
            checkIfPresent(
                loginService.getLoginUser(),
                (loginUser)->
                    checkIfPresent(accountInfoService.getAccountInfo(loginUser).getAge_computed(),
                    (age) -> age < 18
                )
            );
    }

    @Named(Permissions.OVER_20_AND_MORE)
    public Permission over20AndMore() {
        return () ->
            checkIfPresent(
                loginService.getLoginUser(),
                (loginUser)->
                    checkIfPresent(accountInfoService.getAccountInfo(loginUser).getAge_computed(),
                    (age) -> age >= 20
                )
            );
    }
}

このクラスは、前述のアカウント情報について、年齢、課金の状態などを元に、指定した状況とユーザーの持つ状況を比較してコンテンツを見ることが出来るかどうかのPermissionを定義しています。

別のAOPの実装との兼ね合いでやや冗長な@Namedの指定になっていますし、本当はそれぞれのメソッド呼び出しが複数回起きないようにすべきですが、気にしないでください。

さて、このコードがこんなにLambda式だらけに出来るのは、親クラスであるAbstractPermissionManagerにて、次の内部インターフェースが定義されているからです。

    /**
     * provide a permission has the way to check it is granted.
     **/
    public interface Permission {
        boolean isGranted();
    }

    /***
     * check the opt if it's present,else returns false
     */
    protected <T> boolean checkIfPresent(Optional<T> opt, Check<T> check){
        if(!opt.isPresent()){return false;}
        return check.check(opt.get());
    }

    protected interface Check<T>{ boolean check(T obj); }

上記で定義されている、Permission,Checkのように、一組の引数の型と数について一つだけメソッドが定義されているインターフェースは、そのインターフェースをLambda式によって実装を記述し、無名クラス(匿名クラス)として初期化出来ます。

特にCheckprotected boolean checkIfPresent(Optional opt, Check check)の引数です。なお、このメソッドは、与えられたOptionalが値を持つときのみ、その値を引数Checkのメソッドboolean check(T obj)で評価して返します。値がないときはfalseを返します。

なお、このOptionalもJava8から使えるようになった機能です。主に結果があるかどうかが不明なメソッドの返り値の型として利用します。うまく使うとNullチェックを減らしたり、NullPointerExceptionを減らしたり出来ます。

さて、上記のインターフェースについて、ネストが深めのover18AndMore()で使用例を見てみましょう。

    public Permission over18AndMore() {
        return () ->
            checkIfPresent(
                loginService.getLoginUser(),
                (loginUser)->
                    checkIfPresent(accountInfoService.getAccountInfo(loginUser).getAge_computed(),
                    (age) -> age >= 18
                )
            );
    }

over18AndMoreの返却値の型はPermissionです。

ですので、このメソッドの戻り値はLambda式で実装できます。

Lambda式では、式が一行で示される場合、ブロックステートメント{}を省略し、returnも記述不要です。

このメソッドの返り値である無名のPermissionの実装のLambda式はちょうど式が一行で、その式はcheckIfPresentメソッドの呼び出しです。

checkIfPresentの第2引数はCheckなので、これもLambda式で実装されています。

第一引数のOptionalに中身があったときのみ、その評価が行われます。この評価の中で、Optionalが返ってくるメソッドの呼び出し、AccountInfo.getAge_computed()があり、その評価もまたcheckIfPresentを使って、最終的には18才以上なのかをチェックした値が返されます。

この2つのInterfaceとメソッドの定義により、実装の中ではif文はでてきません。

こうした記述方法のメリットは、かなりコードが短くなることです。また、メソッドに名前がついているのでifと評価式で書かれるより、やっている内容がひと目でわかります。

似たような例は、jp.eiya.aya.web.dao.base.UpdateQueryRunnerにもみつけることが出来ます。

ただ、このような実装は、これまでにInterfaceが持っていた、あるメソッドが実装されていることを保証し、再利用性を高めるという役割とやや違っていて、いくらか場当たり的な実装のための手段のように見えます。

なので、この事例のように、interfaceをprotectedやprivateのような狭いスコープに閉じ込めておいたほうが良いでしょう。

Lambda式の使用ポイント その2 後のことを楽にする仕組みの設計

こっちの使用ポイントでは、先ほどと違ってLambdaで実装を記述できるInterfaceを広いスコープ、publicに開放します。

このインターフェースは、jp.eiya.aya.web.util.rest.APIResponseです。

定義はこんなです。

package jp.eiya.aya.web.util.rest;

public interface APIResponse<T> extends ContentResult<T>{
    default boolean success() {return true;}
    default String message(){return "request finished successfully.";}

    static <T> APIResponse<T> failBy(String message,ContentResult<T> contentResult){
        return new APIResponse<T>() {
            public boolean success(){ return false;}
            public String message(){ return message;}
            public T getResult() { return contentResult.getResult();}
        };
    }
}

public interface ContentResult<T> {
    T getResult();
}

このインターフェースは、ResponseBodyとして戻されることを意図しています。デフォルトでは成功を示し、failByによって戻される無名のAPIResponseは失敗を意味します。

インターフェースに対し、デフォルトでは、という使い方ができるのは、これもJava8で新たに登場したインターフェースに実装を書けるdefault methodの仕組みのおかげです。default boolean success() {return true;}の部分がこれに当たります。

さて、これらの使い所はこんな感じ。会員登録のフォームをPOSTしたときのコントローラの振る舞いです。

    @RequestMapping(method = RequestMethod.POST)
    public @ResponseBody APIResponse<RegisterForm> register(@RequestBody RegisterForm form) {
        return tx.execute(status ->{
            boolean success = accountService.create(form.getAccountName(), form.getPassword());
            if (success) {
                Optional<Account> account = accountService.getAccount(form.getAccountName());
                if (account.isPresent()) {
                    success = accountInfoService.upsert(account.get(), ImmutableMap.of(
                            AccountInfo.FIELD_USER_NAME, form.getUserName(),
                            AccountInfo.FIELD_DISPLAY_NAME, form.getDisplayName()
                    ));
                }
            }
            if(!success){
                status.setRollbackOnly();
            }
            return success;
        }) ? () -> form:
             APIResponse.failBy("fail to registration", () -> form);
    }

tx.executeはトランザクションの実行です。これに引数として与えているのもLambda式ですが、これは気にしないでください。

APIResponseの使われどころは、

return success ? () -> form:
                 APIResponse.failBy("fail to registration", () -> form);

この部分です。

これだけで、APIの戻り値として機能する(具体的にはエラーメッセージがヘッダーに入ったりする)型に変換されるようになります。(そのResponseのコンバータの実装終わってたっけかなぁ。。。)

これを行うことでフロントエンド側で幸せな事態を起こすことが出来ます。

このように、後のほうでお約束の書き方を取り入れて便利にしてあげるという仕組みづくりに使うことが出来ます。

問題は、スコープが広いのと、引数になんでも入れられる弱い型制限を取るため間違った使い方を起こしやすいことです。こうした仕組みを作った場合は、どういう目的のために使うのかチームでちゃんとお約束事の勉強をしましょう。

まとめ

  • JavaのWeb開発を最初から始めようとすると、時間がかかる部分は言語の問題ではなさそう。

  • 実装の面ではJava8になってからあらたな書き方、設計が出来る様になった。それは、使い方しだいでは開発を楽にしてくれる。

さて、今回はLambda式の使い所を軽く紹介した程度ですが、実装の面では他にもJava8から追加された色々な機能の恩恵をふんだんに受けています。

それは普段の開発でも同じで、過去の部分を改修する際にはよりエレガントなコードを(これまでよりは)書けるようになっています。

繰り返してきたように、やはりJavaのWeb開発で大変なのは、土台作りの時点だなぁと思いました。

この部分がどうにかなるようなものってのは、なんかJavaの文化を合わないのかもなぁ(Grails流行らないし)とも思いつつ今回はココまで。

どうも、ありがとうございました。


P.S.今回のコード、まともに動くものではありませんが、中身を読んで、ココはこうした方がいいよとかご意見いただけるとありがたいです。

※ ドキュメント中のフォント・・・ゆたぽん (コーディング) フォント (http://net2.system.to/pc/font.html)

P.S.Kotlinでも楽になるのか検証してみました。

Copyright © astamuse company, ltd. all rights reserved.