これは、Java EE Advent Calendar 2014の9日目です。
勢いでAdventCalenderに登録したのですが、ネタが思いつきません。
既出な気もしつつ、JAX-RS2.0のJSONカスタマイズとその
テスト周りに関して書こうと思います。
なお、この実装を行なったのはGlassfish4.0(b89)です。Glassfish4.1ではありませんので、あしからず...。
はじめに
JavaEE7でRESTfulなWebサービスを構築する場合は、標準として提供されるJAX-RS2.0を用います。
JAX-RS2.0の参照実装はデフォルトのJerseyが利用されていますが、Jersyが利用しているJSON変換の実装はMOXyです。
Jacksonの挙動の方がしっくりあうので、Jacksonに変更しておきます。
これで、大体Jacksonの挙動で満足する訳ですが(Listとか)たまに変なデフォルト値の設定を言われて困る訳です。
よくあるパターン:値がnullの場合に空文字を指定したい
Client側にNullを返されるとnull値を考慮する必要があるので、Null値はもれなく空文字で返してほしいというお話です。
この程度であれば、JacksonはNull値の挙動のカスタマイズするための口が準備されているので、そこを拡張して対応します。
@Provider
@Produces(MediaType.APPLICATION_JSON)
public class JacksonObjectMapper implements ContextResolver<ObjectMapper> {
/** ObjectMapper */
private ObjectMapper mapper;
/** デフォルトコンストラクタ. */
public JacksonObjectMapper() {
mapper = new ObjectMapper();
DefaultSerializerProvider.Impl dsp = new DefaultSerializerProvider.Impl();
dsp.setNullValueSerializer(new NullValueSerializer());
mapper.setSerializerProvider(dsp);
}
@Override
public ObjectMapper getContext(Class<?> type) {
return this.mapper;
}
public static class NullValueSerializer extends JsonSerializer<Object> {
@Override
public void serialize(Object t, JsonGenerator jg, SerializerProvider sp)
throws IOException, JsonProcessingException {
jg.writeString("");
}
}
}
プロジェクト毎のレアパターン:何か有象無象したい
たとえが悪いですが。。。型に応じて処理をカスタマイズしたいという話です。
あるBeanの特定の要素のみ対応を入れたいのであれば、個別にAnnotationで制御してやれば良いのですが、
大人の事情で全般的に普通じゃないフォーマットを求められる事があります。
Jacksonは独自のモジュールとフィルターを利用してMixinを行なう事が可能です。
この機能を通して好き勝手に値を書き換えます。
/** デフォルトコンストラクタ. */
public JacksonObjectMapper() {
this.mapper = new ObjectMapper();
// フィルターを登録
FilterProvider filter = new SimpleFilterProvider().addFilter(SampleFilter.FILTER_NAME, new SampleFilter());
this.mapper.setFilters(filter);
// フィルターに対応するモジュールを登録
this.mapper.registerModule(new CustomModule());
}
/**
* カスタムモジュール
*/
private static class CustomModule extends SimpleModule {
private static final long serialVersionUID = 1L;
@Override
public void setupModule(SetupContext context) {
context.setMixInAnnotations(Object.class, PropertyMixIn.class);
}
}
/**
* {@link SampleFilter}を適用するためのMixinクラスです.
*/
@JsonFilter(SampleFilter.FILTER_NAME)
private static class PropertyMixIn {}
private static class SampleFilter extends SimpleBeanPropertyFilter {
/** このフィルター名 */
public static final String FILTER_NAME = "sampleFilter";
public SampleFilter() {}
@Override
protected boolean include(BeanPropertyWriter writer) { return true; }
@Override
protected boolean include(PropertyWriter writer) { return false; }
@Override
public void serializeAsField(Object pojo, JsonGenerator jgen, SerializerProvider provider, PropertyWriter writer) throws Exception {
BeanPropertyWriter beanPropertyWriter = (BeanPropertyWriter) writer;
JavaType javaType = beanPropertyWriter.getType();
if (javaType.isContainerType()) {
// CollectionやMap
// jgen.writeNullField(beanPropertyWriter.get(pojo));
}
// StringやIntegerなど
if (javaType.getRawClass().equals(String.class)) {
//jgen.writeStringField(writer.getName(), beanPropertyWriter.get(pojo));
//return;
}
super.serializeAsField(pojo, jgen, provider, writer);
}
}
テスト
こんな感じで型を弄んでいると当然ユニットテストで確認したくなりますよね。
JAX-RS2.0はJerseyTest
を継承してテストを簡単に行なう事が可能です。
軽量なテストを行なう
Object->JSONのようなResponseの変換に関してテストを行う場合は、
JereyTestで実際にgrizzlyなどを起動して正しく動くかの動作確認を行います。
pom.xmlに必要なモジュールを追加して(Glassfishなのでgrizzlyで)、
<!-- pom.xml -->
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<version>2.7</version>
<scope>test</scope>
</dependency>
テストをこんな感じで書いてさらっと確認が出来ます。
フォーカスしたいのは型変換が適切に動作するかどうかですので、内部クラスとしてObject -> JSONで確認したいパターンを準備しています。
// JerseyTestを継承したテストクラスを作成する
public class JacksonObjectMapperTest extends JerseyTest {
private static final String EMPTY = "";
@Path("/test/")
@Produces(MediaType.APPLICATION_JSON)
public static class Format {
//String変換
@GET @Path("getStringValue")
public String getStringValue() { return "hello"; }
@GET @Path("getStringEmpty")
public String getStringEmpty() { return ""; }
}
@Override
protected Application configure() {
// 処理がトレースしやすいように、ログの詳細化と、実際にClientに返される値のDUMPを有効にする
enable(TestProperties.LOG_TRAFFIC);
enable(TestProperties.DUMP_ENTITY);
// テストに利用する内部クラスと型変換処理をリソースに登録する。
return new ResourceConfig(Format.class, JacksonObjectMapper.class);
}
@Test
public void String値で値が存在する場合はそのまま設定される() {
assertThat(target("/test/getStringValue").request(MediaType.APPLICATION_JSON).get(String.class), is("hello"));
}
@Test
public void String値で値が空の場合は空値が設定される() {
assertThat(target("/test/getStringEmpty").request(MediaType.APPLICATION_JSON).get(String.class), is(EMPTY));
}
}
Resourceクラスをテストする
単純にResourceクラスを単体テストしたいだけであれば、MockitoでCDIのInjectionを隠蔽してしまうと楽です。
CDIをMock化してテストを行う。
本格的にやるのであれば、arquillianあたりを使いましょう(やってないけど)。
きっとAdventCalenderに登録している誰かが書いてくれるはず...。
というか、btnrougeさんがGlassFish 4.1でJAX-RS=CDI連携の単体テストを行う方法
というテーマで GlassFish Advent Calendar 2014で書かれています。
最後に
btnrougeさん、
ネタをずらしてもらってありがとうございます。結局かぶりませんでしたが、ご迷惑おかけしてすいません。
明日は、tkxlabさんです。
以上です。