2016-01-28 追記
Spring boot の場合 Jackson2ObjectMapperBuilder bean を設定するだけでよさそうです。
not boot でも Jackson2ObjectMapperBuilder 経由で ObjectMapper を作ったほうがいろいろいい感じにセットアップされていそう。(個人的にはまどろっこしいなと思いましたが)
refer to https://dzone.com/articles/latest-jackson-integration
なにがしたいのか?
Spring では、プロジェクトに jackson-databind への依存をいれこみ、 @ResponseBody
annotation をコントローラのメソッドにつけると、 JSON を返すことができます。
雑だけどこんな感じの例。 (今回 @RestController
には触れません)
@Controller
public class MainController {
@RequestMapping(value = "/", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public IndexResponse index() {
return IndexResponse.builder().status(200).message("hello").build();
}
@Data
@Builder
public static class IndexResponse {
private int status;
private String message;
}
}
でもこのままだと、デフォルトの ObjectMapper が利用されるので、
{"status":200,"message":"hello"}
みたいに pretty print されてない感じの JSON になってしまう。
で、これをインデントされた JSON で返したりするには、カスタマイズした ObjectMapper を使う必要があるんだけど、それを Spring で利用すればどうすればいいかっていう話です。 (実際に pretty print 形式で返したいかは別として)
ちなみに今回のやりかたを適用すると、 response のほうだけじゃなくて @RequestBody
な request 側にも同じ ObjectMapper が利用されます。
WebMvcConfigurerAdapter での設定
実際に request や response で JSON をハンドリングしているのは HttpMessageConverter
なのですが、カスタマイズした MappingJackson2HttpMessageConverter
を設定するために WebMvcConfigurerAdapter の configureMessageConverters()
を override します。
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
final ObjectMapper objectMapper = new ObjectMapper()
.enable(SerializationFeature.INDENT_OUTPUT);
converters.add(new MappingJackson2HttpMessageConverter(objectMapper));
converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8));
}
}
このやり方、実は Spring の document に書いてあります。 (21.16.12 Message Converters)
(無設定のデフォルトだと StringHttpMessageConverter
と ResourceHttpMessageConverter
と状況に応じて MappingJackson2HttpMessageConverter
が設定されているのですが、 configureMessageConverters()
を override するとそれらのデフォルト設定が無効になるので注意してください。上記サンプルでは StringHttpMessageConverter
を追加してあります。)
これだけで以下のように INDENT_OUTPUT
が適用された JSON 出力が得られます。1
{
"status" : 200,
"message" : "hello"
}
JUnit テストでの設定
上記の Configuration でカスタマイズする方法だと WebMVC Configuration をいじっているだけなので、実際の controller の出力は改善されましたが、通常の JUnit によるテストコードではうまくいきません。
@Test
public void testIndex() {
final MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(jsonPath("$.status").value(200)) // => OK
.andExpect(jsonPath("$.message").value("hello")) // => OK
.andReturn();
log.info("{}", result.getResponse().getContentAsString());
// => {"status":200,"message":"hello"}
// ^ INDENT_OUTPUT が効いていない
}
ではどうすればいいのか。
MockMvc
を作成するときに StandaloneMockMvcBuilder
を使うと、 MessageConverter
を指定できるのでそれを利用します。
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
public class MainControllerTest {
@Autowired
private MainController mainController;
private MockMvc mockMvc;
@Before
public void setUp() throws Exception {
final ObjectMapper objectMapper = new ObjectMapper()
.enable(SerializationFeature.INDENT_OUTPUT);
mockMvc = MockMvcBuilders.standaloneSetup(mainController)
.setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper))
.build();
}
@Test
public void testIndex() throws Exception {
// ......
}
}
サンプルでよくある MockMvcBuilders
の webAppContextSetup()
を使うと、
DefaultMockMvcBuilder
が利用されてしまいカスタム MessageConverter
を設定する方法がありません。 (この場合のやり方がわかる方、教えてください)
おまけ: lombok-jackson を Spring で使う場合
Lombok の @Value
class のインスタンスを jackson で deserialize する場合、通常であればコンストラクタを自前で実装し、 @JsonProperty
annotation をコンストラクタ引数に付与していく必要があります。
こんなとき、 lombok-jackson を使うと、自動的に上記のように annotation を付与したコンストラクタを生成してくれます。 (くわしくは lombok-jackson を参照してください)
ただこれ、利用するときに dependency に追加するだけではだめで、 ObjectMapper
に一手間くわえる必要があります。(上記プロジェクトのドキュメントから抜粋)
new ObjectMapper()
.setAnnotationIntrospector(new JacksonLombokAnnotationIntrospector());
と、いうことで、これも以下のように MessageConverter
を設定する技を使うと Spring から使うことができます。
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
final ObjectMapper objectMapper = new ObjectMapper()
.setAnnotationIntrospector(new JacksonLombokAnnotationIntrospector());
converters.add(new MappingJackson2HttpMessageConverter(objectMapper));
super.configureMessageConverters(converters);
}