JAX-RS と JSON と List と hibernate

おさらい

REST api を自作してると response として List を含んだ JSON を返したいということがよくあるが、簡単なサンプルがなかったりするので、簡単なやつを作ってみた。


@Path("/persons")
public class SimpleResource {
 
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Person> list() {
        
        Person person1 = new Person();
        Person person2 = new Person();
        person1.setName("秋葉");
        person2.setName("akiba");
        List<Person> persons = new ArrayList<>();
        persons.add(person1);
        persons.add(person2);
        
        return persons;
        
    }
    
    private class Person{
        
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
    
}

これをアプリケーションサーバにデプロイしてブラウザで http://…/persons にアクセスすると

[{"name":"秋葉"},{"name":"akiba"}]

という文字列が表示される。

だから何?って言われそうだが、やってないとけっこう忘れますw

返値の型は List<Person> なので、JSON で配列を表す [] が先にきて、インスタンスが二つあるので {} が二つ続くと。

 

この程度なら単純なのだが、ややこしくなってくるのは、Person クラス自体にコレクションを含むような場合だ。

ちょっとやってみよう。


@Path("/persons")
public class SimpleResource {
 
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Person> list() {
        
        Person person1 = new Person();
        Person person2 = new Person();
        person1.setName("秋葉");
        person2.setName("akiba");
        person1.addfriend("上野");person1.addfriend("新橋");person1.addfriend("神田");
        List<Person> persons = new ArrayList<>();
        persons.add(person1);
        persons.add(person2);
        
        return persons;
        
    }
    
    private class Person{
        
        private String name;
     List<String> friends = new ArrayList<>();

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public void addfriend(String name){
            friends.add(name);
        }

        public List getFriends() {
            return friends;
        }

        public void setFriends(List friends) {
            this.friends = friends;
        } 

    }
    
}

Person クラスに String の List friends を追加した。

この場合のブラウザ出力は

[{"name":"秋葉","friends":["上野","新橋","神田"]},{"name":"akiba","friends":[]}]

となる。

本題 (インスタンスの List を hibernate で操作 )

実用的なシステムを組む場合、上の friends 相当箇所が別のクラス(エンティティ)になる。

Friend というクラスがあって、List<Friend> friends を定義する、みたいな。

この場合、hibernate を組み込んで、@OneToMany だののアノテーションをつけることになると思うが、巷の教科書的な解説はアノテーションの付け方を変に複雑にしているせいで、わかりにくくなっている。

次の記事でシンプルなサンプルを提示する予定。

コンテクストルート

ところで、REST を作る最も簡単なやり方は

@ApplicationPath("resources")
public class JakartaRestConfiguration extends Application {

}

とすることだが、このとき @GET などのアノテーションをつけたクラスの URI は

http:// (サーバIP)/rest-1.0/resources/

となってなんかダサい。war のファイル名になるのを回避するには

<?xml version="1.0" encoding="UTF-8"?>
<jboss-web>
<context-root>/rest</context-root>
</jboss-web>
jboss-web.xml

というファイルを WEB-INF においておけばいい。この場合は、context-root で指定した文字列がコンテクストルートになる。

http:// (サーバIP)/rest/resources/

こっちの方がスッキリしますね。

 

CDI と weld の関係

Java の習得は、SE → EE の順に進むと思うが、EE になって役者がごそっと出てくるため、躓きやすい。

これまでにも EJBJPA の話題は取り上げてきたが、今回は CDI 。

まず、CDI と weld の関係だが、CDI が仕様で weld がその実装の一つ。

WildFly の stanalone.xml などで

        <extension module="org.jboss.as.weld"/>

とあるのは、この WildFly には weld を組み込んでいますよ、という意味だ。

WildFly 27 では、weld 5.1 が採用されているので、その概略は

Weld 5.1.0.Final – CDI Reference Implementation

でチェックするといいだろう。

下手な日本語解説記事よりわかりやすい。

CDI とは?

ところで CDI とは何だろう?
よく依存性注入がどうしたこうしたと難しげに解説してあるサイトがあるが、初学者が理解できるとは思えない。
簡単に言えば、あるオブジェクトを使うのにいちいち new して作成せずともコンテナの方で勝手にやってくれる機能のことだ。

注入されるクラスにはそのインスタンスの生存期間を明示する必要があるので scope 関係のアノテーションを付与し、実際に注入する際には @Inject アノテーションでその箇所を明記する。

なお、CDI は Contexts and Dependency Injection の頭文字をつなげたもの。

サンプルコード解説

いつものように簡単なサンプル作成。

インジェクトされるクラスとインジェクトするクラス(サーブレット)を作成し、使用通りに動くか検証。

インジェクトされる側のクラスは以下の通り。

ScopedSevice.java
------
@RequestScoped
public class ScopedService {

    private int counter = 0;

    public void countUp() {
        this.counter++;
    }

    public int getCounter() {
        return this.counter;
    }
}

これを以下のサーブレットクラスにインジェクトする(今回はフィールドにそのままインジェクト)する。

CDI.java
-------
public class CDI extends HttpServlet {

    @Inject
    ScopedService scopedService;

    @Override
    protected void doGet(final HttpServletRequest pReq, final HttpServletResponse pResp) throws ServletException, IOException {
        this.scopedService.countUp();
        this.scopedService.countUp();
        pResp.setContentType("text/plain");
        pResp.getWriter().println("Counter Value -> " + this.scopedService.getCounter());
    }
}

ScopedService クラスは new などで明示的にインスタンス化されていないが、CDI クラスから問題なく使えるか試すコードです。
インジェクトされた後、countUp メソッドを2回呼び出しているので、counter = 2 となっていれば、良いわけです。
サーブレットなので、ブラウザから呼び出すと

と見事に予想通りの処理をしてくれています。

ソースコードは github にあげてあります。

面白かったのは、Inject される側のクラスで @Singleton を使うと以下のようなエラーが出た点だ。

Dispatcher error: {"WFLYCTL0080: Failed services" => {"jboss.deployment.unit.\"CDI-Jakarta10-1.0.war\".WeldStartService" => "Failed to start service
Caused by: org.jboss.weld.exceptions.DeploymentException: WELD-001408: Unsatisfied dependencies for type ScopedService with qualifiers @Default
at injection point [BackedAnnotatedField] @Inject servlet.CDI.scopedService
at servlet.CDI.scopedService(CDI.java:0)
"}}

CDI を使う利点の一つは、スコープの管理が楽になることなので、安直に singleton は使わない方がいいのかもしれません。

(追記)ここ、ワイ、勘違いしてたかも。
@Statless, @Stateful, @Singleton は EJB の文脈で用いられるアノテーションなので、単純に CDI では使えないってことなのかも。

参考

ここ

Unfortunately, there’s a little problem with this pseudo-scope. Beans with scope @Singleton don’t have a proxy object.

CDI の仕様が変われば、うまく扱えなくなること確実なアノテーション。それが @Singleton www

 

Jakarta EE 10 時代到来か? WildFly 27, GlassFish 7, Payara 6

WildFly 27

WildFly 27 がリリースされた。

WildFly 27 では、JakartaEE 10 のみの対応となるので、今後は本格的に JakartaEE 10 の時代となりそう。

気になる JakartaEE 9.1 → 10 に伴う仕様の変更は、ざっくりいうと以下の図のような感じ。

CDI 関連が大幅変更。CDI Lite 4.0 (橙色)が新規に追加され、従来の CDI も 4.0 にアップデートされた。
ここらへんはわかりにくいところなので、『CDI と weld の関係』にまとめる予定。

他の仕様もかなりの部分がアップデート(青色)されている。

Servlet が 6.0 になっているのに恐怖を感じるが(笑)、そろそろ移行作業を開始しましょうかね。

GlassFish 7

GlassFish も JakartaEE 10 に対応した GlassFish 7 をリリースしている。

JakartaEE 10 環境、整ってきました。

ただし、

NOTE: The latest milestone version doesn’t provide Admin Console (GUI for administering the server). This will be fixed in the final version of GlassFish 7.

ということなので、現在配布されているバージョン(M8)はウェブアプリを GUI コンソール画面からデプロイすることはできない

実際、起動後 loalhost:4848 にアクセスしても以下の画面から進まない。

どうしても使いたい場合は、github リポジトリからソースを取ってきて自力でビルドするしかないだろう。

ただし、ビルド産物は M7 で、これが本当に JakartaEE 10 に対応しているかどうかは不明。

Payara 6

Payara 6 はコミュニティ版も Jakarta EE 10 に対応しているらしい。

payara は気にはなっていたのだが、触ったのは今回が初めて。

しかし、GlassFish とまるっきり一緒だと思っていたのだが、コンソール画面も微妙にカスタマイズ入ってますね。

ブログの記事なども充実しており、乗り換えてもいいかも。

まとめ

JakartaEE 10 対応のアプリケーションサーバーも出揃ってきた。

特に payara に好印象を持った。

今回は、アプリケーションサーバーの紹介程度になってしまったので、JakartaEE 10 の仕様・使い方に関しては順次別記事にまとめていく予定。

 

 

 

JakartaEE 10 に備えよ -まずは 9.1 に移行しておくのが吉-

最近は、ウェブアプリの類は、意識して JakartaEE のお作法で書くようにしている。

といっても JavaEE の頃と大した違いがあるわけではない。

アイキャッチにあるように、これまで

javax.***

としていたところを

Jakarta.***

としているだけ。

これは、JakartaEE 9.1 では JavaEE の名前空間を変えただけ程度の変更に留まっているから。

だから、既存の JavaEE プロジェクトでも IDE などを使って javax → jakarta と「置換」すれば、ほぼ移行作業は完了する。

「ほぼ」と書いたのは、もちろん例外もあるから。

ここでハマったという人が多いようだが、

javax.sql.Datasource

は、このままでいい。

このパッケージは EE の仕様ではなくて SE の仕様なので、Jakarta に変更する必要はないから。

言われてみれば当たり前なのだが、脳死状態で移行していると気が付きにくいですね。

私も、小一時間ハマりましたw

JakartaEE 10 では、新機能も追加されるだろうから、今くらいの時期(ちなみにこれ書いているのは 2022/11)から移行しておいた方がいいでしょう。

JavaEE → JakartaEE 10 の移行は、一気にステージが二段階上がるようなものだから、まずはここで手がかりを作っておくのが吉、という読みです。

(参考)JakartaEE の XML ファイルのスキーマは、ここ参照。
9.1 → 10 で変化するところはけっこうある。
例えば web-app は 5.0 → 6.0 。

(追記)理屈の上では、現在(2022/12)使うべきは JakartaEE 10 なのだろうが、いくつかのアプリケーションサーバを試してみたが、hibernate 経由で clob を扱う際に不具合が出やすいという印象がある。
ちょっと慣れておく、アタリをつける程度の使用にとどめておいた方がいいかもしれない。

 

Java/Jakarta EE 対応サーバとは?

正式には、このページをご参照ください。

WildFly, GlassFish といった有名どころのほか、最近では中華系のアプリケーションサーバも Java/JkartaEE に対応しているようだ。

最近では payara (パイアラと読むらしいです。GlassFish の派生版)の話をたまに聞く。

ところで、たまに「特定の環境でビルドした war ファイルはどのアプリケーションサーバでも動くか?」という質問をうけるんですが、ほとんどの場合(特にシステムが複雑になればなるほど)、手直しなしでそのまま動くことはないです。

簡単なウェブアプリであっても設定などを微妙に変えないといけない場合がほとんどです。
Mac で GlassFish を動かす』あたりをご参照ください。

また、各アプリケーションサーバによって Java/JakartaEE のモジュールの実装が違うため(例えば、JAX-RS の実装は GlassFish では jersey、WildFly では resteasy がデフォルト)、この場合は、結構な量の手直しが必要です。