JAX-RSとかの話
これは2013-04-19に行われた「いいね!Java EE!」で使用した資料を加筆修正したものです。
JAX-RSって?
- Webアプリを作るためのAPI
- JSR 311 (JAX-RS 1.x)
- JSR 339 (JAX-RS 2.x)
- Java EE 6 Full Profile に入ってる(なぜかWeb Profileじゃない)
- Java EE 7 からは Web Profile に入る
- Jersey(参照実装)、RESTEasy、Apache CXFなどの実装があり、みんな大好きTomcatでも使える
- 仕様書(PDF)は41ページで目に優しい(ちなみにEJB 3.1は626ページ)
開発準備
- NetBeans を使いましょう!
以上
Maven
Mavenでアレするなら次のようなdependencyを書けば良いです。
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-bundle</artifactId>
<version>1.11.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.sun.jersey.jersey-test-framework</groupId>
<artifactId>jersey-test-framework-http</artifactId>
<version>1.11.1</version>
<scope>test</scope>
</dependency>
Jerseyのartifactはjersey-serverやjersey-jsonなどいくつかに分かれているのですが、jersey-bundleはそれらがまとめられたもので、こいつを指定するのが楽ちんです。
jersey-test-framework-httpはJerseyのテスティングフレームワークで、Hotspotに入ってるcom.sun.net.httpserver.HttpServer で実行します。
artifactId の末尾の -http を -grizzly にするとGrizzly(GlassFishのHTTPを捌く部分)で動かす事もできますし、-inmemory にするとソケットを介さずインメモリで動かす事もできます。
はじめてのJAX-RS
クエリパラメータで名前を渡すとHello, xxx!が返るWeb APIを書きましょう。
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
@Path("hello")
public class HelloResource {
@GET
public String say(
@QueryParam("name") String name) {
return "Hello, " + name + "!";
}
}
これは次のようなHTTPリクエストを処理できます。
GET /rest/hello?name=world HTTP/1.1
/rest
がアプリケーションのルートとなります。
これは後述するApplicationサブクラスに注釈する@ApplicationPath
で指定する値が対応します。
あとは何となく見たら分かる感じではありますが、/hello
が @Path("hello")
に、?name=world
が @QueryParam("name")
に、それからリクエストメソッドがGETですが @GET
が対応しています。
こいつをとりあえずサクッと動かすならjersey-test-frameworkを使うのがらくちんです。
JUnitでぶん回すことができます。
import com.sun.jersey.test.framework.AppDescriptor;
import com.sun.jersey.test.framework.JerseyTest;
import com.sun.jersey.test.framework.LowLevelAppDescriptor.Builder;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
public class HelloResourceTest extends JerseyTest {
@Test
public void test_say() throws Exception {
String response = resource()
.path("hello")
.queryParam("name", "world")
.get(String.class);
assertThat(response, is("Hello, world!"));
}
@Override
protected AppDescriptor configure() {
return new Builder(HelloResource.class).build();
}
}
アプリケーションサーバやサーブレットコンテナで動かす
GlassFish などのJava EEアプリケーションサーバで動かすにはApplicationサブクラスを作ります。
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("rest")
public class JaxrsActivator extends Application {
}
Application
を継承して@ApplicationPath
で注釈するだけです。
あとはHelloResource
とこのJaxrsActivator
をWARにパッケージングしてデプロイすればおkです。
Tomcatなどのサーブレットコンテナだと専用のサーブレットを登録する必要がある......と見せかけてServlet 3対応のコンテナならJerseyのJARを突っ込むだけでweb.xml
を書く必要はありません。
web.xml
を書かなくてもServletContainerInitializer
を利用して動的にサーブレットを追加してくれます。
リソースクラス、リソースメソッド
リソースクラスは@Path
で注釈したクラスで先の例でいうとHelloResource
がリソースクラスになります。
クラス名に制約はないのでHello
でもFoobar
でも何でも良いです。
@Path
にはこのリソースクラスで処理するパスを指定します。
リソースクラスはpublicなコンストラクタが必要です。
@Path("hello")
public class HelloResource { ... }
リソースメソッドはリソースクラスに定義されたメソッドで@GET
や@POST
といったHTTPメソッドに対応するアノテーションで注釈します。
リソースメソッドでは@Consumes
で受け取るリクエストボディのContent-Type
を指定できます。
同じように@Produces
で送り返すレスポンスボディのContent-Type
を指定できます。
また引数に@QueryParam
や@HeaderParam
を注釈することでクエリパラメータやリクエストヘッダをマッピングできます。
@GET
@Consumes("text/plain")
@Produces("text/plain")
public String say(
@QueryParam("name") @DefaultValue("world") String name) {
return "Hello, " + name + "!";
}
なお、@QueryParam
などでマッピング出来るのはリソースメソッドの引数だけじゃなくコンストラクタの引数や
@Path("hello")
public class HelloResource {
private final String name;
public HelloResource(
@QueryParam("name") String name) {
this.name = name;
}
...
フィールド、
@Path("hello")
public class HelloResource {
@QueryParam("name")
private String name;
...
setterなども使用できます。
@Path("hello")
public class HelloResource {
private String name;
@QueryParam("name")
public void setName(String name) {
this.name = name;
}
...
個人的にはメソッドの引数を使用するのが好きです。
パラメータのマッピング
既にクエリパラメータを@QueryParam
でマッピングする例は挙げましたが、他にはリクエストヘッダやパスの一部、Cookieの値などをアノテーションでマッピングできます。
@QueryParam
- クエリパラメータをマッピング
- /hoge?name=value
@GET
public String sayHello(
@QueryParam("name") String name) {
...
@FormParam
- フォームのPOSTリクエストで送信するパラメータをマッピング
- <input type="text" name="name">
@POST
public String sayHello(
@FormParam("name") String name) {
...
@PathParam
- パスの一部をマッピング
- /hoge/value
@GET
@Path("{name}")
public String sayHello(
@PathParam("name") String name) {
...
コロンで区切って正規表現を書く事もできます。
@GET
@Path("{id:[0-9]{1,10}}")
public String findById(
@PathParam("id") Long id) {
...
@MatrixParam
- マトリックスパラメータ
- セミコロンで区切った形式
- /hoge;foo=1;bar=2
@GET
@Produces("text/plain")
public String sayHello(
@MatrixParam("left") String left,
@MatrixParam("right") String right) {
...
@CookieParam
- Cookieをマッピング
@GET
@Produces("text/plain")
public String sayHello(
@Cookie("name") String name) {
...
@HeaderParam
- リクエストヘッダをマッピング
@GET
@Produces("text/plain")
public String sayHello(
@HeaderParam("name") String name) {
...
パラメータをまとめる
パラメータがひとつふたつなら良いですが、もっと多くなると引数に列挙するのはシグネチャがうるさくなりますね。
Jerseyなら@InjectParam
を使うことでパラメータをPOJOにまとめることができます。
ただし、JAX-RSの仕様じゃなくてJerseyの実装依存の機能ですので、そこんとこ注意です。
@GET
@Produces("text/plain")
public String sayHello(
@InjectParam HogeBean bean) {
...
public class HogeBean {
@QueryParam("foo")
public String foo;
@QueryParam("bar")
public String bar;
...
ちなみにJAX-RS 2からはこの@InjectParam
と同様の機能をもつ@BeanParam
というアノテーションが追加されます。
パラメータをPOJOで受け取る
ここまで@QueryParam
などで受け取るパラメータの型はString
を使用していましたが、自作のクラスを使用することも可能です。
パラメータを受け取れるクラスは、valueOf
またはfromString
という名前の静的ファクトリメソッドを定義する必要があります。
引数はString
です。
public class Fullname {
...
//public static Fullname valueOf(String value) {
public static Fullname fromString(String value) {
return new Fullname(value);
}
}
リソースメソッドではString
でパラメータを受けるときと同じ感覚で使えます。
@GET
@Produces("text/plain")
public String sayHello(
@QueryParam("name") Fullname name) {
...
XMLで通信する
エンティティボディがXMLの場合、JAXBで自作のクラスにマッピングする機能がJAX-RSにはあります。
例えばこのようなXMLを、
<hogeBean>
<foo>hello</foo>
<bar>world</bar>
</hogeBean>
このようなクラスで受け取ることが可能です。
@XmlRootElement
はJAXBのAPIです。
@XmlRootElement
public class HogeBean {
public String foo;
public String bar;
}
リソースメソッドはこのようになります。
@Consumes
でXMLを受け取る事を明示しています。
@POST
@Consumes("application/xml")
public void doHoge(HogeBean bean) {
...
レスポンスをXMLにすることも可能です。
その場合、リソースメソッドは次のようになります。
今度は@Produces
でXMLを返すことを明示しています。
@GET
@Produces("application/xml")
public HogeBean getHoge() {
...
前述の通りXMLとクラスの相互変換を行う部分はJAX-RSではなくJAXBの仕様です。
JAXBはJava SEに入っているので動作確認は手軽にできます。
HogeBean obj = ...
StringWriter out = new StringWriter();
JAXB.marshal(obj, out);
String xml = out.toString();
...
String xml = ...
StringReader in = new StringReader(xml);
HogeBean obj = JAXB.unmarshal(xml, HogeBean.class);
JSONで通信する
前述のようにJAXBでXML通信している場合、Jerseyなら@Consumes
や@Produces
でのメディアタイプの指定をapplication/json
に変更するだけでJSONで通信することが可能です。
@POST
//@Consumes("application/xml")
//@Produces("application/xml")
@Consumes("application/json")
@Produces("application/json")
public HogeBean doHoge(HogeBean bean) {
...
元々はXMLで通信していましたが、たったこれだけで次のようなJSONで通信するようになります。
{ "foo" : "hello",
"bar" : "world" }
JSON通信での問題点
リストを含む次のようなクラスの、
@XmlRootElement
public class Hoge {
public List<String> foobar;
}
インスタンスを作成して、
Hoge obj = new Hoge();
obj.foobar = Arrays.asList("a", "b", "c");
XML通信すると次のようなXMLになります。
<hoge>
<foobar>a</foobar>
<foobar>b</foobar>
<foobar>c</foobar>
</hoge>
foobar要素がリストの要素分、フラットに並んでいますね。
これがJSON通信の場合だと次のようなJSONになります。
{ "foobar" : [ "a", "b", "c" ] }
XMLではフラットに並んでいたfoobar要素が空気を読んでリストになっていますね。
で、次はこんな感じのインスタンスを、
Hoge obj = new Hoge();
obj.foobar = Arrays.asList("x");
JSON通信すると次のようなJSONになります。
{ "foobar" : "x" }
リストじゃなくなっていますね。 なんでやねん、と。
foobar要素が1つのXMLを想像するとなんとなく納得できます。
<hoge>
<foobar>x</foobar>
</hoge>
このように、要素がひとつだとリストなのかリストじゃないのか分からないのです。
JerseyのJAXB経由のJSON変換は、XMLに変換する過程に横入りして行っているのでこの影響をモロに受けてリストじゃなくなってしまうっぽいです。
クライアント側もJerseyを使っていたりするとこれが問題になることは無いですが、WebアプリでjQueryなんかを使ってたりするとリストのつもりで受け取ったらリストじゃなかったでござる、という状況になって困ります。
というか困りました、実際に。
JacksonでJSON通信する
前述の「要素がひとつの場合にリストじゃなくなっちゃう問題」に対処するにはJacksonを利用したJSON変換を行うと良いです。
Jacksonを使うとfoobar要素がひとつしかない場合でも次のようなJSONに変換されます。
{ "foobar" : [ "x" ] }
また、Jacksonを使うと、
- クラスを
@XmlRootElement
で注釈する必要がない - リソースメソッドの戻り値を
java.util.List
にする事が可能
などの利点があります。
JerseyでJacksonを使うには初期パラメータ com.sun.jersey.api.json.POJOMappingFeature
を true
に設定します。
jersey-test-frameworkを使ったりJDKのHttpServer
で動かす場合はResourceConfig
というクラスで設定すると良いです。
ResourceConfig rc = ...
rc.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, true);
書くまでもないですが、このコードにあるJSONConfiguration.FEATURE_POJO_MAPPING
は文字列の定数で、"com.sun.jersey.json.POJOMappingFeature"
です。
サーブレット経由で動かすならweb.xml
で設定することも可能です。
<servlet>
<servlet-name>Jersey</servlet-name>
<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
<param-value>true</param-value>
</init-param>
</servlet>
個人的にはJerseyでJSON通信するならJacksonを使うのが良いと思います。
XMLでもJSONでも通信する
これまでXMLかJSONのどちらか片方で通信する設定方法を紹介しましたが、ひとつのメソッドでXMLでもJSONでも通信することも可能です。
設定は単純で@Consumes
や@Produces
に複数のメディアタイプを書けば良いです。
@POST
@Consumes({ "application/json", "application/xml" })
@Produces({ "application/json", "application/xml" })
public HogeBean doHoge(HogeBean bean) {
...
このリソースメソッドはクライアントがAcceptヘッダでapplication/json
を要求すればJSONで通信し、application/xml
を要求すればXMLで通信します。
このようにメソッドの内容はまったく同じだけど、HTTPリクエストの内容によって通信のフォーマットを切り替えられるのはJAX-RSの強みですね。
MessageBodyReader/MessageBodyWriter
JAX-RSで通信できるのはXMLやJSONだけではありません。
MessageBodyReader
やMessageBodyWriter
を実装すればエンティティボディを好きにマッピングすることが可能です。
例えば、String[][]
をCSVで出力するMessageBodyWriter
を実装してみます。
@Provider
@Produces("text/csv")
public class CsvWriter implements MessageBodyWriter<String[][]> {
@Override
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return type == String[][].class;
}
@Override
public long getSize(String[][] t, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return -1;
}
@Override
public void writeTo(String[][] t, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException, WebApplicationException {
try (PrintWriter out = new PrintWriter(entityStream)) {
for (String[] row : t) {
for (String column : row) {
out.printf("%s,", column);
}
out.println();
}
}
}
}
@Provider
で注釈していますが、これを付けておくとアノテーションスキャンで拾ってくれます。
あるいはクラスパス上にMETA-INF/services/javax.ws.rs.ext.MessageBodyWriter
というファイルを作って、中にCsvWriter
のFQCNを書いてServiceLoader
に拾ってもらいます。
@Produces
で注釈することで、このMessageBodyWriter
が処理する対象となるContent-Type
を指定しています。
さらにisWritable
メソッドでこのMessageBodyWriter
を使うべきか判断することが可能です。
getSize
メソッドは書き出すエンティティボディのバイトサイズです。
算出できない(し辛い)場合は-1
を返しておくと良きに計らってくれます。
最後にwriteTo
メソッドですが、これが実際にエンティティボディに書き出すメソッドになります。
このCsvWriter
に対応するレスポンスを返すリソースメソッドはこんな感じです。
@GET
@Produces("text/csv")
public String[][] getCsv() {
...
オブジェクトから実際の通信形式へ変換する方法をMessageBodyWriter
に分離しているのでリソースメソッドはシンプルに保たれていますね。
WebApplicationException
場合によってはリソースメソッドで処理中に「やっぱり404返したいわー」などというときもあると思いますが、WebApplicationException
を投げるのが楽ちんです。
@GET
@Path("{isbn}")
@Produces("application/json")
public Book get(@PathParam("isbn") Isbn isbn) {
Book book = bookBean.get(isbn);
if(book == null) {
throw new WebApplicationException(404);
}
...
ExceptionMapper
リソースメソッドから投げられた特定の例外を受け取って処理したい場合はExceptionMapper
を実装したサブクラスを作ります。
例えばJPAの楽観排他機能で更新したいエンティティが既に別のひとに更新されていた場合、OptimisticLockException
が投げられますが、これを受け取って処理をするExceptionMapperを書いてみます。
@Provider
public class OptimisticLockExceptionMapper implements ExceptionMapper<OptimisticLockException> {
@Override
public Response toResponse(OptimisticLockException exception) {
return Response.status(400).entity("更新されとった><").type("text/plain").build();
}
}
@Contextで色々な情報を参照する
前述のOptimisticLockExceptionMapper
ですが、EJBを使っている場合はOptimisticLockException
がRollbackException
に包まれて投げられます。
RollbackException
はOptimisticLockException
と継承関係は無いのでOptimisticLockExceptionMapper
で処理できません。
そんな場合はProviders
を使います。
public static class RollbackExceptionMapper implements ExceptionMapper<RollbackException> {
@Context
private Providers p;
@Override
public Response toResponse(RollbackException exception) {
Throwable cause = exception.getCause();
Class<Throwable> type = (Class<Throwable>) cause.getClass();
return p.getExceptionMapper(type).toResponse(cause);
}
}
@Context
でProviders
をインジェクションします。
Providers
はクラスやアノテーションを渡すとそれに対応するExceptionMapper
やMessageBodyReader
を取ってこれる便利なものです。
このような便利クラスを@Context
でインジェクションできます。
インジェクションできる便利クラスはProviders
の他にクエリパラメータやパスの情報を取って来れるUriInfo
や、HTTPヘッダを取れるHttpHeaders
、認証情報を取れるSecurityContext
などがあります。
他にもDIしたい
EJBでDI
リソースクラスをStateless Session Beanにします。 @EJB
でSession Beanを、PersistenceContext
でEntityManager
などをインジェクションできます。
@Path("hello")
@Stateless
public class HelloResource {
@EJB
private HelloBean helloBean;
@GET
public String say(@QueryParam("name") String name) {
return helloBean.say(name);
}
}
個人的な利点は宣言的トランザクションでしょうか。
欠点はSession BeanかEntityManager
ぐらいしかDIするものがないことですかね。
CDIでDI
WEB-INF/beans.xml
を作成します。 空のファイルでもOKです。
@Path("hello")
@RequestScoped
public class HelloResource {
@Inject
private HelloBean helloBean;
@GET
public String say(@QueryParam("name") String name) {
return helloBean.say(name);
}
}
基本的に何でもDIできるのが利点です。
CDIでは殆どのクラスが管理Beanになります。
欠点はEJBでは使えていた宣言的トランザクション使えないことです。
まあ自分でCDIのインターセプターを書いて適用すれば良いんですが、ちょっと面倒です。
EJBとCDIを併用する
というわけでEJBとCDIを併用します。
@Stateless
@Path("hello")
public class HelloResource {
@Inject
private HelloBean helloBean;
@GET
public String say(@QueryParam("name") String name) {
return helloBean.say(name);
}
}
利点はDIを@Inject
で統一できることです。
CDI管理Beanは当然ですが、(少し手を加える必要がありますが)EntityManager
もDataSource
も@Inject
でぶっ込むことが可能です。
欠点はJersey 1.11.1のバグです。
その他、DIの利点
- インターセプタをかませる(JAX-RS 2.0からはJAX-RSの仕様にインターセプターが入りますが)
- モックにすげ替えやすい
- Arquillianでテストしやすい
Arquillian
ArquillianはJBossが提供しているJava EE向けの結合テストフレームワークです。
以下の例のようにテストコードを書く事が可能です。
@RunWith(Arquillian.class)
public class CalcTest {
@Inject
CalcBean calcBean;
@Test
public void test_add() throws Exception {
int answer = calcBean.add(2, 3);
assertThat(answer, is(5));
}
@Deployment
public static WebArchive createDeployment() {
return ShrinkWrap.create(WebArchive.class)
.addClass(CalcBean.class)
.addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml");
}
JAX-RS 2.0の新機能
JAX-RS 2.0からは以下のような機能が追加されます。
- フィルター
- インターセプター
- 非同期処理
- クライアントAPI
- Bean Validationとの統合
フィルター
リクエスト・レスポンスそれぞれに対応するフィルターを書けます。
@Provider
public class LoggingFilter implements ContainerRequestFilter, ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
Logger.getLogger("request");
}
@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) throws IOException {
Logger.getLogger("response");
}
}
インターセプター
エンティティボディを読み書きするところに横入りしてごにょごにょできます。
@Provider
public class StarInterceptor implements ReaderInterceptor, WriterInterceptor {
@Override
public Object aroundReadFrom(ReaderInterceptorContext context) throws IOException,
WebApplicationException {
Object entity = context.proceed();
return "+" + entity + "+";
}
@Override
public void aroundWriteTo(WriterInterceptorContext context) throws IOException,
WebApplicationException {
Object entity = context.getEntity();
context.setEntity("*" + entity + "*");
context.proceed();
}
}
アノテーションで適用する範囲を決める
CDIも似たような感じですが、フィルター(インターセプター)にアノテーションを付けておくと同じアノテーションが付いているリソースクラス・リソースメソッドにそのフィルター(インターセプター)を噛ませることが出来るようです。
@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,
ElementType.METHOD})
public static @interface Logged {
}
@Logged
@Provider
public class LoggingFilter implements ContainerRequestFilter, ContainerResponseFilter {
...
リソースメソッドはこんな感じ。
@POST
@Logged
public String post(String s) {
...
非同期処理
Servlet 3.xにも非同期処理が入りましたが、JAX-RSにもやってきました。
以下のサンプルはJSR 339に載っていたものです。
private static final BlockingQueue<AsyncResponse> suspended =
new ArrayBlockingQueue<AsyncResponse>(5);
@GET
@Produces("text/plain")
public void readMessage(@Suspended AsyncResponse ar) throws InterruptedException {
suspended.put(ar);
}
@POST
@Produces("text/plain")
public String postMessage(final String message) throws InterruptedException {
final AsyncResponse ar = suspended.take();
ar.resume(message); // resumes the processing of one GET request
return "Message sent";
}
うん、使いどころがわかりません!
使ってみたい
というわけで、JAX-RS 2.0のリリースはまだですが、いち早く試したい場合はJersey 2.xを使ってみましょう!
<dependencies>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
<version>2.0-rc1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-jdk-http</artifactId>
<version>2.0-rc1</version>
<scope>test</scope>
</dependency>
</dependencies>
まとめ
- JAX-RSいいね!
- もうServletは要らないですね
- ところで「いいね!Java EE!」の第2回はあるんですかね?
資料の手直し、最後の方疲れたので投げやりです。
まあ、またJAX-RSは話題に出すと思いますので書ききれなかった(書き忘れた)アレとかソレはまたの機会にー。
Discussion