はじめに
java で RESTful サービスを作ろうと思い Jersey を使う事にした。
実用的なサンプルを見つけたが、実際に動かした記事が見つからないので作ってみた。
前提と目的
使うものは、
- 開発ツールは Eclipse
- JAX-RSの実装は Jersey
- WEBサーバーは Tomcat
- データベースは PostgreSQL
- JPAは EclipseLink
Bookmark サンプルの概要については以下に説明されている。
Jerseyブックマーク・サンプルについて
JAX-RS は仕様であり、実装の一つとして Jersey がある。
JAX-RS は Java EE の仕様であるが、Java SE を使う Tomcat でも動かせる。
Spring 等の特定のフレームワークを使う必要は無い。
JPAも特に必要では無いが、bookmark サンプルが使っていたので入っている。
Bookmarkサンプルの概要説明を読んで理解しても良いが、実際に動かせたほうが理解しやすいと思った。
Eclipse で Jersey を動かすプロジェクトの作成
Eclipse のメニューから「ファイル」「新規」「Mavenプロジェクト」を選択し、アーキタイプに「jersey-quickstart-webapp」を指定してプロジェクトを作成する。
ネット上では古い記事が多く思った通りに動くとは限らない。
ここでは 前章で作成したプロジェクトをベースに構築している。
pom.xml で jersey-container-servlet に修正しておくことを忘れずに。
この記事では、Eclipse(2024年)、Tomcat10、Java21、PostgreSQL15 という構成を使用している。
bookmark サンプルのソースをプロジェクトに追加する
ソースコードは Maven で公開されている。
https://repo1.maven.org/maven2/org/glassfish/jersey/examples/bookmark/4.0.0-M1/
bookmark-4.0.0-M1-sources.jar
このバージョンは Tomcat10 以降でしか動かない事に注意。
Tomcat9 以前で使いたければ 2.x を使う。
ダウンロードして展開
src/main/java に comフォルダーをコピー
src/main/resources に META-INFフォルダーをコピー
完成時は以下の様な構成になる。
POM.xml に不足分を追加
(コンパイルエラーを潰していくと必然的に不足分が解る)
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>jakarta.transaction</groupId>
<artifactId>jakarta.transaction-api</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jettison</artifactId>
<version>4.0.0-M1</version>
</dependency>
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
<version>4.0.0</version>
</dependency>
JPAについて
個人的には、O/Rマッピングとか好きではなく、JDBCでゴリゴリ書きたい向きではある。
JPAはJDBCを使うのが面倒だという人、何でもオブジェクト化しないと気が済まない人向けの仕組みとしか理解していない。
エンティティが中核だが、このクラスがデータベースのテーブルに対応する。
アプリケーションはJDBCを意識することなくこのエンティティを操作する事で裏ではデータベースと同期しているイメージ。
エンティティの実体はPOJO(Java Bean)で、アノテーションでデータベース上のテーブルと関連付けられる。
ちゃんとした説明が必要な人は以下のような記事を参考にされると良い。
JPAの基礎1
BookmarkサンプルのJPA
何はともあれ、実際に使用するデータベースのJDBCドライバーと紐づけしないと始まらない。
JPAの仕組みでは、これは persistence.xml に記述する。
このファイルの場所は、アプリケーションの META-INF 直下に置く。
(WEBアプリでは、{コンテキストパス}/WEB-INF/classes/META-INF の下)
サンプルの persistence.xml に記載されている内容は、
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
<persistence-unit name="BookmarkPU" transaction-type="JTA">
<provider>oracle.toplink.essentials.PersistenceProvider</provider>
<properties>
<property name="toplink.jdbc.url" value="jdbc:derby://localhost:1527/BookmarkDB"/>
<property name="toplink.jdbc.user" value="REST"/>
<property name="toplink.jdbc.driver" value="org.apache.derby.jdbc.ClientDriver"/>
<property name="toplink.jdbc.password" value="REST"/>
<property name="toplink.ddl-generation" value="drop-and-create-tables"/>
</properties>
</persistence-unit>
</persistence>
サンプルではデータベースとして Apache Derby を対象としている。
私は derby 自体に見識が無いので、ここを PostgreSQL に変えれば済むだろう。
ところで、toplink って何?
どうも Oracle 社が関係している JPA の実装のようで無償版(essentials)のようだが、はっきり言って使う気にならない。今は EclipseLink を使うのが良いのだろう。
JPAにEclipseLinkを使用するように書き換え
<persistence-unit name="BookmarkPU" >
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="org.postgresql.Driver"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:postgresql://127.0.0.1:5432/testdb"/>
<property name="jakarta.persistence.jdbc.user" value="postgres"/>
<property name="jakarta.persistence.jdbc.password" value="postgres"/>
</properties>
</persistence-unit>
pom.xml に追加する。
<!-- https://mvnrepository.com/artifact/org.eclipse.persistence/eclipselink -->
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>3.0.4</version>
</dependency>
EclipseのTomcatへのJDBCドライバの登録
Tomcat で使う場合には、JDBCドライバはサーバー側に持たせておくほうが良いだろう。
JNDIも使えるし、個別のアプリケーションごとにPOM.XMLに記載する必要が無くなる。
方法としては、JDBCドライバのjarファイルを Eclipse の Tomcat のインストール先のディレクトリ(lib)に直接コピーする。
場所はデフォルトなら、
{EclipseインストールDIR}/tomcat/{バージョン}/lib
JNDIの設定
server.xml と context.xml を編集する。
場所は、例えば Tomcat10 で Java21 の環境なら
{EclipseインストールDIR}/workspace/Servers/Tomcat10_Java21-config/
server.xml の例
<GlobalNamingResources>
<Resource name="jdbc/testdb" auth="Container" type="javax.sql.DataSource"
username="postgres"
password="postgres"
driverClassName="org.postgresql.Driver"
url="jdbc:postgresql://localhost:5432/testdb"
maxTotal="60"
maxIdle="20"
validationQuery="select 1" />
</GlobalNamingResources>
context.xml の例
<Context>
<ResourceLink name="jdbc/testdb"
global="jdbc/testdb"
auth="Container"
type="javax.sql.DataSource" />
</Context>
PostgreSQL の準備
用意するテーブルとカラムは以下の通り。
create table public."users" (
userid text not null
, username text
, password text
, email text
)
create table public."bookmarks" (
bmid text not null
, userid text
, sdesc text
, ldesc text
, updated text
, uri text
)
テストデータも数件登録しておくと良い。
実行してみる
その前に、API の basePath をどうするか。
Tomcat を使うとコンテキストパスがあるが、これは host の一部という位置づけになる。
Bookmark サンプルでは、MyApplication.java が、
@ApplicationPath("/")
public class MyApplication extends ResourceConfig {
となっている。
プロジェクト作成時の index.jsp を消していないが起動時に何も表示されないのは嫌なので残しておいて良い。
ただ、web.xml は消しておかないとURI の Path がバッティングする。
(テスト用の Myresource.java も不要なのでフォルダごと消しておくと良い)
本番環境ではこれでも良いが、テストでは初期表示ができないとか不便な面もある。
"/api" とかに変えても良い。
こうすると、index.jsp が実行されるので、必要なリンク等を追加しておくと気分的に良い。
ここでは、プロジェクト名を "bookmark" basePath を "/api" とした。
Eclipseで「実行」し表示されたブラウザで、
localhost:8081/bookmark/api/users/
とすると、下記のエラー。
jakarta.servlet.ServletException: java.lang.NullPointerException: Cannot invoke "jakarta.persistence.EntityManagerFactory.createEntityManager()" because "this.emf" is null
URIが上手く認識されているのは良いとして、エラーの場所は、
@PersistenceUnit(unitName = "BookmarkPU")
EntityManagerFactory emf ;
という事が解る。当初 persistence.xml の記載が悪いと思っていろいろ試行錯誤していたのだが、
結局はアノテーションが働いていないのが原因であると解った。
つまり、
// @PersistenceUnit(unitName = "BookmarkPU")
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("BookmarkPU");
と明示的に書く。
何のためのアノテーションなのか!注釈は所詮、解釈されなければ何の意味も無いと悟った。
Firefoxで見たところは、以下の様な動きになる。
実際のデータは json 形式で返ってきている。
Tomcat9 を使う場合
パッケージの名前空間が jakarta ではなく、javax を使う。
これによって、使用するライブラリのバージョンも変わるので注意が必要。
また、Java11 以降では Jersey2.x は動かない。(不具合が出る) Java8 を使う必要がある。
例えば、次のような一見 JPA が悪いかのようなエラーが出る。
javax.servlet.ServletException: A MultiException has 2 exceptions. They are:
1. javax.persistence.PersistenceException: No Persistence provider for EntityManager named BookmarkPU
2. java.lang.IllegalStateException: Unable to perform operation: create on org.glassfish.jersey.examples.bookmark.resource.UsersResource
実際には、Tomcat9 の JRE を Java8 に変更すると何事も無く正常に動作する。
JTA が動作しない
検索は正常に動くものの、更新では JTA が設定されていないと動かない。
JTA は 分散トランザクション を制御する機構。JPA の更新処理ではトランザクションは必須である。
JTA は通常 WEBコンテナに実装されているが、Tomcat では無いので、別途モジュールを追加する必要がある。
検索した限りでは、JOTM が見つかるが、組み込もうとしても Tomcat 起動時にエラーが出て動作しない。
2009年頃から更新されていないので、今でもまともに動くと考えるほうが無理だろう。
そもそも JavaSE に分散トランザクションは不要だろう。
JTA を使わない方法として persistence.xml 内の transaction-type を "RESOURCE_LOCAL"にする。
<persistence-unit name="BookmarkPU" transaction-type="RESOURCE_LOCAL">
プログラムも修正する必要がある。
サンプルの TransactionManager.java を次のように書き換える。
public final class TransactionManager {
public static void manage(Transactional t) {
// UserTransaction utx = getUtx();
EntityTransaction utx = t.em.getTransaction();
try {
utx.begin();
// if (t.joinTransaction) {
// t.em.joinTransaction();
// }
t.transact();
utx.commit();
} catch (Exception e) {
// try {
utx.rollback();
// } catch (SystemException se) {
// throw new WebApplicationException(se);
// }
throw new WebApplicationException(e);
} finally {
t.em.close();
}
}
}
以上で、更新処理も動作する。
最後に
ソースと実際の動きを対比することで理解は格段に深まる。
構築には苦労したが、見識は深まったので良しとしよう。