はじめに
Jersey の環境構築までの記事は良くありますが、実装についての参考になるような良いサンプルが見つかりません。以前書いたものは、動作させるだけで終わってしまいました。
もっと簡単に、要点だけまとめたかったので、自分でサンプルを書いてみました。
概要
ここでは、なるべくシンプルで応用が利くものを、と考えました。
フレームワークは使わず、データ操作も JPA 等のO/Rマッパーを使わず JDBC をそのまま使っています。
機能
データベースの任意のテーブルのデータをAPIで参照・追加・更新・削除ができる。
設計
エンドポイントとしては、
/api/tables/{tableid}/records/{recordid}
処理は、下記の通りです。
URL | METHOD | 機能 | 詳細 |
---|---|---|---|
/api/tables | GET | 照会 | テーブル一覧 |
/api/tables/{tableid} | GET | 照会 | カラム一覧 |
/api/tables/{tableid}/records | GET | 照会 | レコード情報(複数行) |
/api/tables/{tableid}/records/{recordid} | GET | 照会 | レコード情報(1行) |
/api/tables/{tableid}/records/ | POST | 追加 | レコード追加 |
/api/tables/{tableid}/records/{recordid} | PUT | 更新 | レコード更新 |
/api/tables/{tableid}/records/{recordid} | DELETE | 削除 | レコード削除 |
環境
- Tomcat10
- Jersey4.0
- PostgreSQL15.0
- JDK17
- Eclipse(Pleiades 2024)
環境構築と全ソースは、Eclipse のプロジェクトを GitHub に上げてあります。
Eclipse でクローンしていただければ動くと思います。
Swagger-UI を使うと、定義を自動生成しているので、エンドポイントのテストが簡単に行えます。
ソースファイルの構成
次のフォルダに分けています。
- リソース(resource)
- エンティティ(entity)
- 例外(exception)
- その他(util)
実装の肝は、主にリソースです。
リソースの分類とその役割
図にしてみました。
リソースの分類
クラスには、ルートリソースとサブリソースという分類があります。
だいたい下記の様な理解で足りると思います。
- class の外に@Path があれば、ルートリソース
- class の中に@GET 等のメソッドがあれば、サブリソース
リソースの中のメソッドには、次のものがあります。
- class 内で@Path だけがあるメソッドは、サブリソースロケータ
- class 内で@Path と@GET等があるメソッドは、サブリソースメソッド
- class 内で@GET等の動詞だけがあるものは、リソースソッド
サブリソースロケータは、サブリソースを呼び出します。
何故、このような区別が必要かと言えば、上の図のように、jersey エンジンが行っている処理がそれぞれで違っているからです。
エンドポイントの URI を /path1/path2/path3/.... のような階層にする場合、ルートリソース内でサブリソースメソッドだけで書いていくのも可能ですが、非常に解り難くなってしまいます。
そこで、機能ごとに、サブリソースに分け、芋づる式に呼んだほうが、冗長性が無く、拡張性にも優れています。
ルートリソース
サンプルのソースを見ていきたいと思います。
エンドポイントの /api/tables で呼ばれる部分です。
/**
* Root Resource Class
*/
@Path("tables")
public class TablesResource {
@Path("{tableid}/")
public TableResource getTable(@PathParam("tableid") String tableid) {
return new TableResource(tableid);
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public Tables getTables() {
Tables oTables = null;
try ( Connection conn = JdbcUtil.getConnection() ) {
oTables = new Tables( conn );
} catch (Exception e) {
throw new ExtendedWebApplicationException("tables error!");
}
return oTables;
}
}
クラス内の@Path のメソッドは、サブリソースをインスタンス化して return で返しているのに注目してください。
対して、@GET メソッドでは、エンティティである(Tables)のオブジェクトを返しています。
つまり、検索したデータを返している訳です。
ルートリソースのインスタンス化は jersey が行っています。
コンストラクタを書く事も可能です。
リソースソッド
@GET があるメソッドです。
@Produces(MediaType.APPLICATION_JSON) があることによって、戻り値が JOSN 文字列に変換されてレスポンスとして返されます。
以下は、その戻されるエンティティのクラスです。
public class Tables {
public ArrayList<String> tables = new ArrayList<>();
public Tables(Connection conn ) throws SQLException {
DatabaseMetaData meta= conn.getMetaData();
ResultSet rs = meta.getTables(null, "public", null, null);
while( rs.next() ) {
tables.add(rs.getString("TABLE_NAME"));
}
rs.close();
}
}
インスタンス化された時のコンストラクタでデータベース検索を行い、ArrayList に格納しているだけです。
結果は、このオブジェクトの変数部分が JOSN 文字列に変換されて返されます。
変換処理を書かなくて良いので簡単ですね。
サブリソースロケーター
@Path("{tableid}/") のメソッドです。
サブクラスを呼び出すメソッドをサブリソースロケーターと言います。
インスタンスを自分で作成している点に注目して下さい。
ルートリソースと違って、jersey は作成してくれません。サブリソース内のメソッドは、呼び出してくれます。
同じリソース内のメソッドでも、戻り値の役割が違うことを理解しましょう。
サブリソース
ルートリソースから呼ばれるサブリソースです。
URI の /api/tables/{tableid}/ で呼ばれる部分です。
/*
* Sub Resource Class
*/
public class TableResource {
String tableid = null;
TableResource(String tableid ) {
this.tableid = tableid;
}
@Path("records")
public RecordsResource getRecords() {
return new RecordsResource(tableid);
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public InfoTable getInfoTable() {
InfoTable oInfo = null;
try (Connection conn = JdbcUtil.getConnection() ) {
oInfo = new InfoTable( conn, tableid );
} catch (Exception e) {
throw new ExtendedWebApplicationException("info table error!");
}
return oInfo;
}
}
ここでも、/api/tables/{tableid}/records/ の場合は、さらにサブリソースを呼び出しています。
この理屈が理解できれば、いくらでもエンドポイントを深くすることができます。
@GET メソッドでは、エンティティ(InfoTable)のオブジェクトを返しています。
テーブルのカラム情報が得られます。
他のサブクラスも大体同じ様な感じです。
ルートリソース⇒サブリソース⇒サブリソース...と順番に処理されている点も頭に入れておくと良いと思います。
共通部分はコンストラクタで行っておけば済みます。
JSON の受け方
補足ですが、@POST 等で JSON データを受け取る場合です。
/*
* 単一レコード操作サブリソース
*/
public class RecordResource {
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void updateRecord( UpdateRecord update) {
try (Connection conn = JdbcUtil.getConnection()) {
update.execute( conn, tableid, recordid );
} catch (Exception e) {
throw new ExtendedWebApplicationException("update error!");
}
}
@Consumes を指定してあるので、
- リクエストが JSON 形式かどうかをチェック
- 引数のクラスのオブジェクトに変換する
を自動で行ってくれます。
ここでは、エンティティのクラスを直接引数にしています。
インスタンス化してくれるので、後は実行するだけです。
少々強引に感じたら、String で受けて、Jackson の ObjectMapper で変換する事もできます。
以上が、リソースについての説明になります。
例外(Exception)処理
以下の適切なものを Throw します。
http のステータスに対応しているので、メッセージを付けておけば、デバッグにもなります。
その他
ユーティリティ的なものを書いておくと良いと思います。
データベースのコネクションの取得をここに書いています。
Servlet 使用時と全く変わりません。
テストする際は、ご自分の環境に合わせて書き換えて下さい。
認証
特に認証については、今はトークン認証(OAuth2.0やJWT)が主流ですから、JAX-RS がサポートするようなセッションを使う認証はあまり使われないと思います。
認証はピンキリなので、ここでは扱いません。
言える事は、filter は使わないほうが良いと思います。
そもそも全体であれば、ルートリソースだけで行えば良い事は、仕組み上から理解していただけると思います。
CORS 対応
忘れがちですが、API を外部から利用しようとすると、ハマる問題です。
例えば、Swagger-UI を直接ブラウザからファイルで立ち上げて使おうと思っても、何もしないと次のようなエラーになります。
JavaScript から API を呼び出す際にブラウザのセキュリティが働くのでこのようになります。
対応方法にはいろいろな方法があります。
jersey 側で対応すると、Filter で実装するのが簡単なようです。
public static class CORSFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
response.getHeaders().putSingle("Access-Control-Allow-Origin", "*");
response.getHeaders().putSingle("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, DELETE");
response.getHeaders().putSingle("Access-Control-Allow-Headers", "Content-Type");
}
}
Application クラスに登録します。
(Originだけでも大丈夫)
register(CORSFilter.class);
全ての応答に、ヘッダーが含まれてしまうのが、少し難点です。
Swagger の利用
エンドポイントが複雑になると、テストが面倒になります。
Jersey に Swagger Core を追加する事で、定義が自動で生成されます。
サンプルソースには組み込んでいるので、Swagger UI で、
/api/openapi.yaml にアクセスすれば、エンドポイントが全部見られます。
これが無かったら、検証する気になれないと思います。
動作テスト
PostgreSQL上に適当なテーブルを作成してください。
- スキーマは"public"
- テーブルには、"id" というキーになるカラムが存在している前提
- 入出力は文字列を前提にしているので変換できないデータ型には未対応
- 英小文字のテーブル名、カラム名しかテストしていない
おわりに
エンティティをJDBCで書く事で、Jersey の骨格が良く理解できるようになったと思います。