Jersey JAX-RS (JSR-311) のメモ

JerseyをServlet 3.0と一緒に使う方法のメモです。
JavaでREST APIを実装するのにすごく便利なので、みんなも使うと良いと思うよ。

リソースクラスのライフサイクル

リクエストごとにリソースクラスのインスタンスが生成される。@Singletonアノテーションで制御可能。

POSTされたファイルを受け取る

以下のリソースクラスを宣言し、

@Path("/add")
public class Add {
	private static final Logger LOG = LoggerFactory.getLogger(Add.class);
 
	@POST
	@Consumes(MediaType.TEXT_XML)
	public Response invoke(String aXml) {
		LOG.info("{}", aXml);
		return Response.ok().build();
	}
}

curlでファイルをPOSTすればaXmlにtest.xmlの内容を受け取ることができる。

$ curl http://localhost:8080/add --data-binary @test.xml -H 'Content-Type: text/xml; charset=utf-8'

test.xmlが巨大な場合は、InputStreamとして受け取るようにすればストリームとして処理可能。

@POST
@Consumes(MediaType.TEXT_XML)
public Response invoke(InputStream aXmlInputStream) {
	....
}

JSPへ処理を中継する

ViewableをEntityとして返却すればOK。
Viewableコンストラクタの第2引数には任意のオブジェクトを渡すことができ、JSPからitとして参照できる。

package jp.kurusugawa.hoge;
public class TestResouce {
	@GET
	@Produces(MediaType.TEXT_HTML)
	public Response list() {
		....
		return Response.ok().entity(new Viewable("test.jsp", "ほげほげ").type(MediaType.TEXT_HTML).build();
	}
}

JSPファイルはパッケージ名とクラス名に対応したディレクトリに配置する必要がある。
上の例ではコンテントルート(WebContent)/jp/kurusugawa/hoge/TestResource/test.jspとなる。

<html>
<head><title>test</title></head>
<body><p>${it}</p></body>
</html>

XML, JSON, JSONPオブジェクトを返却する

以下のようにJAXBでアノテーションしたクラスを作り、

@XmlRootElement(name = "names")
public class Names {
	private List<String> mNames;
 
	// デシリアライズの為にデフォルトコンストラクタが必要
	public Names() {
	}
 
	public Names(List<String> aNames) {
		mNames = aNames;
	}
 
	@XmlAttribute(name = "count")
	public int getCount() {
		return mNames.size();
	}
 
	@XmlElement(name = "name")
	public List<String> getNames() {
		return mNames;
	}
}

以下のようにオブジェクトをリターンすれば、jerseyとjersey-json(+jackson-mapper)が自動的にXML, JSON, JSONPに変換してくれる。

@GET
public Names get() {
	return new Names(Arrays.asList("squld", "mel", "sho"));
}
 
// JSONPのみJSONWithPaddingでラップしたオブジェクトを返却する必要あり
@Path("jsonp")
@GET
public JSONWithPadding jsonp(@QueryParam("callback") @DefaultValue("callback") String aCallback) {
	return new JSONWithPadding(new Names(Arrays.asList("squld", "mel", "sho")), aCallback);
}

試しに、curlでAcceptヘッダを指定して呼び出してみると、期待したフォーマットでオブジェクトを受け取ることができた。

XML返却を期待する呼び出し。

$ curl -H 'Accept: application/xml' http://localhost:8080/name
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><names count="3"><name>squld</name><name>mel</name><name>sho</name></names>

JSON返却を期待する呼び出し。

$ curl -H 'Accept: application/json' http://localhost:8080/name
{"@count":"3","name":["squld","mel","sho"]}

JSONP返却を期待する呼び出しは、JSONWithPaddingでラップしたjsonp関数を呼べばOK。

$ curl -H 'Accept: application/javascript' http://localhost:8080/name/jsonp
callback({"@count":"3","name":["squld","mel","sho"]})

JSONP返却を期待してget関数のほうを呼び出すと、返り値がJSONWithPaddingでラップされていないため、JSONPにマッピングできず以下のエラーになるので注意!

javax.ws.rs.WebApplicationException: com.sun.jersey.api.MessageException: A message body writer for Java class jsr311.NameResource$Names, and Java type class jsr311.NameResource$Names, and MIME media type application/octet-stream was not found

また、jsonp関数はXML, JSON, JSONP全部のパターンで呼び出しが成功するので、効率考えなければJSONWithPaddingで常にラップしておいてもOK。

$ curl -H 'Accept: application/xml' http://localhost:8080/name/jsonp
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><names count="3"><name>squld</name><name>mel</name><name>sho</name></names>
$ curl -H 'Accept: application/json' http://localhost:8080/name/jsonp
{"@count":"3","name":["squld","mel","sho"]}

HTTPのレスポンスコードを指定したい場合は、以下のようにResponseにエンティティとして指定すればOK。

@GET
public Response get() {
	return Response.status(Status.INTERNAL_SERVER_ERROR).entity(new Names(Arrays.asList("squld", "mel", "sho"))).build();
}

JSONで要素数が1つの配列を返却する

先ほどのNamesクラスに対して要素を1つだけ持たせたものをJSONにエンコードすると

@GET
public Names get() {
	return new Names(Arrays.asList("squld"));
}

以下のように、nameの値が文字列の配列ではなく文字列になってしまう。

$ curl -H 'Accept: application/json' http://localhost:8080/name
{"@count":"1","name":"squld"}

これを解消するには、以下のようなクラスを追加してJAXBContextProviderを作りnameが配列であることを明示すれば良い。

@Provider
public class JAXBContextResolver implements ContextResolver<JAXBContext> {
	private final List<Class<?>> mTargetTypes;
	private final JAXBContext mContext;
 
	public JAXBContextResolver() throws Exception {
		// @formatter:off
		final Class<?>[] tTargetTypes = {
			Names.class,
		};
		// @formatter:on
 
		mTargetTypes = Arrays.asList(tTargetTypes);
 
		// @formatter:off
		mContext = new JSONJAXBContext(
			JSONConfiguration
			.mapped()
			.rootUnwrapping(true)
			.arrays(
				"name"
			)
			.build(),
			tTargetTypes
		);
		// @formatter:on
	}
 
	public JAXBContext getContext(Class<?> aObjectType) {
		return mTargetTypes.contains(aObjectType) ? mContext : null;
	}
}

@Providerアノテーションがついているので、自動的に有効になります。
再度アクセスしてみると、ちゃんと配列になった。

$ curl -H 'Accept: application/json' http://localhost:8080/name
{"@count":"1","name":"[squld"]}

特定のURLパターンで静的コンテンツを返す

Jerseyを使っている場合は、@WebFilter@WebInitParamとして、静的コンテンツを返却したいURLを正規表現で与えることでフィルタ対象から除外できる。
以下の例では、/((image|css|js)/.*|favicon.ico)にマッチするURLは静的コンテンツとして処理される。

@WebFilter(urlPatterns = { "/*" }, initParams = { @WebInitParam(name = "com.sun.jersey.config.property.WebPageContentRegex", value = "/((image|css|js)/.*|favicon.ico)") })
public class Application extends ServletContainer {
 ... 以下略

オブジェクトをインジェクトする

Jersey自体がDIの機能を持ってるので、使うと楽できる。
jersey-guiceとかとどうやって棲み分けるべきか悩みどころ。
普通は依存ライブラリが少ないほうが良いので、問題が無い限りはJersey自体のDIを使うと良いと思う。

以下のようにConfigクラスのプロバイダクラスを定義して、

@Provider
public class ConfigProvider implements InjectableProvider<Context, Type> {
	public static class Config {
		... 省略 ...
	}
 
	private final Config mConfig;
 
	public ConfigProvider() {
		mConfig = new Config();
	}
 
	@Override
	public Injectable<Config> getInjectable(ComponentContext aComponentContext, Context aAnnotation, Type aType) {
		if (Config.class != aType) {
			return null;
		}
		return new Injectable<Config>() {
			@Override
			public Config getValue() {
				return mConfig;
			}
		};
	}
 
	@Override
	public ComponentScope getScope() {
		return ComponentScope.Undefined;
	}
}

以下のように@Contextアノテーションを使って、好きなところでインジェクト指定すればOK。

@GET
public Names get(@Context Config aConfig) {
	return new Names(aConfig.getMembers());
}

Maven 2から使う

org.codehaus.mojo.archetypes/webapp-javaee6を元にして以下のPOMファイルを作ればOK。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
 
  <groupId>jp.kurusugawa</groupId>
  <artifactId>jsr311</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>
 
  <name>jsr311</name>
 
  <properties>
    <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
 
  <repositories>
    <repository>
      <id>maven2-repository.dev.java.net</id>
      <name>Java.net Repository for maven</name>
      <url>http://download.java.net/maven/2/</url>
    </repository>
  </repositories>
 
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.8.2</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>javax</groupId>
      <artifactId>javaee-web-api</artifactId>
      <version>6.0</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-core</artifactId>
      <version>1.6</version>
    </dependency>
    <dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-server</artifactId>
      <version>1.6</version>
    </dependency>
    <dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-json</artifactId>
      <version>1.6</version>
    </dependency>
  </dependencies>
 
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.2</version>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
          <compilerArguments>
            <endorseddirs>${endorsed.dir}</endorseddirs>
          </compilerArguments>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>2.1.1</version>
        <configuration>
          <failOnMissingWebXml>false</failOnMissingWebXml>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-dependency-plugin</artifactId>
        <version>2.1</version>
        <executions>
          <execution>
            <phase>validate</phase>
            <goals>
              <goal>copy</goal>
            </goals>
            <configuration>
              <outputDirectory>${endorsed.dir}</outputDirectory>
              <silent>true</silent>
              <artifactItems>
                <artifactItem>
                  <groupId>javax</groupId>
                  <artifactId>javaee-endorsed-api</artifactId>
                  <version>6.0</version>
                  <type>jar</type>
                </artifactItem>
              </artifactItems>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
 
</project>

参考URL

  • 3.1.1 Lifecycle and Environment – JSR311 spec JCP 1.1 Final Release
    By default a new resource class instance is created for each request to that resource. First the constructor (see section 3.1.2) is called, then any requested dependencies are injected (see section 3.2), then the appropriate method (see section 3.3) is invoked and finally the object is made available for garbage collection.
    An implementation MAY offer other resource class lifecycles, mechanisms for specifying these are outside the scope of this specification. E.g. an implementation based on an inversion-of-control framework may support all of the lifecycle options provided by that framework.
  • How to better transfer objects between Jersey restful enabled classes to JSP pages
  • Java Architecture for XML Binding
    Java Architecture for XML Binding(JAXB)は、Javaのクラスを XMLで表現可能にする仕様である。
  • Injected into hell
    I’m still trying to get Jersey to inject a class I provide into resource classes.
  • Example 5.10. JSON expression produced using mapped notation
    To fix this issue, you need to instruct the JSON processor, what items need to be treated as arrays by setting an optional property, arrays, on your JSONConfiguration object. For our case, you would do it with

    JSONConfiguration.mapped().arrays("addresses").build()

コメントをどうぞ