MappingJacksonHttpMessageConverter
とSpringの@JsonView
と@JsonFilter
アノテーションを使用しながら、Jackson @ResponseBody
と@RequestBody
アノテーションを使用して、Spring MVCコントローラーから返されるJSONを変更できますか?
public class Product
{
private Integer id;
private Set<ProductDescription> descriptions;
private BigDecimal price;
...
}
public class ProductDescription
{
private Integer id;
private Language language;
private String name;
private String summary;
private String lifeStory;
...
}
クライアントがProducts
のコレクションを要求すると、各ProductDescription
の最小バージョン、おそらくそのIDだけが返されます。その後、後続の呼び出しで、クライアントはこのIDを使用して、すべてのプロパティが存在するProductDescription
の完全なインスタンスを要求できます。
呼び出されたメソッドは、クライアントがデータを要求していたコンテキストを定義するため、Spring MVCコントローラーメソッドでこれを指定できることが理想的です。
この問題は解決しました!
フォロー これ
Jacksonシリアル化ビューのサポートを追加します
Spring MVCは、Jackonのシリアライゼーションビューをサポートするようになりました。異なるコントローラーメソッドから同じPOJOの異なるサブセットをレンダリングするためです(たとえば、詳細ページと概要ビュー)。問題:SPR-7156
これは SPR-7156 です。
ステータス:解決済み
説明
ジャクソンのJSONViewアノテーションを使用すると、開発者はメソッドのどの側面をシリアライズするかを制御できます。現在の実装では、Jacksonビューライターを使用する必要がありますが、コンテンツタイプは使用できません。 RequestBodyアノテーションの一部として、JSONViewを指定できるとよいでしょう。
Spring ver> = 4.1で利用可能
この link をたどってください。 @JsonViewアノテーションの例で説明します。
最終的には、JAX-RSに対してStaxManが示したものと同様の表記法を使用します。残念ながら、Springはそのままではこれをサポートしていないので、私たち自身で行う必要があります。
これは私の解決策ですが、あまりきれいではありませんが、うまくいきます。
@JsonView(ViewId.class)
@RequestMapping(value="get", method=RequestMethod.GET) // Spring controller annotation
public Pojo getPojo(@RequestValue Long id)
{
return new Pojo(id);
}
public class JsonViewAwareJsonView extends MappingJacksonJsonView {
private ObjectMapper objectMapper = new ObjectMapper();
private boolean prefixJson = false;
private JsonEncoding encoding = JsonEncoding.UTF8;
@Override
public void setPrefixJson(boolean prefixJson) {
super.setPrefixJson(prefixJson);
this.prefixJson = prefixJson;
}
@Override
public void setEncoding(JsonEncoding encoding) {
super.setEncoding(encoding);
this.encoding = encoding;
}
@Override
public void setObjectMapper(ObjectMapper objectMapper) {
super.setObjectMapper(objectMapper);
this.objectMapper = objectMapper;
}
@Override
protected void renderMergedOutputModel(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
Class<?> jsonView = null;
if(model.containsKey("json.JsonView")){
Class<?>[] allJsonViews = (Class<?>[]) model.remove("json.JsonView");
if(allJsonViews.length == 1)
jsonView = allJsonViews[0];
}
Object value = filterModel(model);
JsonGenerator generator =
this.objectMapper.getJsonFactory().createJsonGenerator(response.getOutputStream(), this.encoding);
if (this.prefixJson) {
generator.writeRaw("{} && ");
}
if(jsonView != null){
SerializationConfig config = this.objectMapper.getSerializationConfig();
config = config.withView(jsonView);
this.objectMapper.writeValue(generator, value, config);
}
else
this.objectMapper.writeValue(generator, value);
}
}
public class JsonViewInterceptor extends HandlerInterceptorAdapter
{
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
JsonView jsonViewAnnotation = handlerMethod.getMethodAnnotation(JsonView.class);
if(jsonViewAnnotation != null)
modelAndView.addObject("json.JsonView", jsonViewAnnotation.value());
}
}
Spring-servlet.xml内
<bean name="ViewResolver" class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<property name="mediaTypes">
<map>
<entry key="json" value="application/json" />
</map>
</property>
<property name="defaultContentType" value="application/json" />
<property name="defaultViews">
<list>
<bean class="com.mycompany.myproject.JsonViewAwareJsonView">
</bean>
</list>
</property>
</bean>
そして
<mvc:interceptors>
<bean class="com.mycompany.myproject.JsonViewInterceptor" />
</mvc:interceptors>
Springでどのように機能するかはわかりませんが(申し訳ありません!)、Jackson 1.9はJAX-RSメソッドから@JsonViewアノテーションを使用できるため、次のことができます。
@JsonView(ViewId.class)
@GET // and other JAX-RS annotations
public Pojo resourceMethod()
{
return new Pojo();
}
ジャクソンは、ViewId.classで識別されるビューをアクティブビューとして使用します。おそらく、Springには同様の機能がありますか? JAX-RSでは、これは標準のJacksonJaxrsProviderによって処理されます。
同じ答えを探して、ResponseBodyオブジェクトをビューでラップするというアイデアを思いつきました。
コントローラクラスの一部:
@RequestMapping(value="/{id}", headers="Accept=application/json", method= RequestMethod.GET)
public @ResponseBody ResponseBodyWrapper getCompany(HttpServletResponse response, @PathVariable Long id){
ResponseBodyWrapper responseBody = new ResponseBodyWrapper(companyService.get(id),Views.Owner.class);
return responseBody;
}
public class ResponseBodyWrapper {
private Object object;
private Class<?> view;
public ResponseBodyWrapper(Object object, Class<?> view) {
this.object = object;
this.view = view;
}
public Object getObject() {
return object;
}
public void setObject(Object object) {
this.object = object;
}
@JsonIgnore
public Class<?> getView() {
return view;
}
@JsonIgnore
public void setView(Class<?> view) {
this.view = view;
}
}
次に、writeInternal
メソッドフォームMappingJackson2HttpMessageConverter
をオーバーライドして、シリアル化するオブジェクトがinstanceofラッパーであるかどうかを確認します。そうである場合は、必要なビューでオブジェクトをシリアル化します。
public class CustomMappingJackson2 extends MappingJackson2HttpMessageConverter {
private ObjectMapper objectMapper = new ObjectMapper();
private boolean prefixJson;
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
JsonGenerator jsonGenerator =
this.objectMapper.getJsonFactory().createJsonGenerator(outputMessage.getBody(), encoding);
try {
if (this.prefixJson) {
jsonGenerator.writeRaw("{} && ");
}
if(object instanceof ResponseBodyWrapper){
ResponseBodyWrapper responseBody = (ResponseBodyWrapper) object;
this.objectMapper.writerWithView(responseBody.getView()).writeValue(jsonGenerator, responseBody.getObject());
}else{
this.objectMapper.writeValue(jsonGenerator, object);
}
}
catch (IOException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
}
}
public void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
this.objectMapper = objectMapper;
super.setObjectMapper(objectMapper);
}
public ObjectMapper getObjectMapper() {
return this.objectMapper;
}
public void setPrefixJson(boolean prefixJson) {
this.prefixJson = prefixJson;
super.setPrefixJson(prefixJson);
}
}
多くの頭が行き止まりでオタクの怒りのかんしゃくを叩いた後のこれに対する答えはとても簡単です。この使用例では、複雑なオブジェクトAddressが埋め込まれたCustomer Beanがあり、jsonシリアル化が行われるときに、アドレス内のプロパティ名surburb and streetのシリアル化を防止します。
これを行うには、フィールドのアドレスに注釈@ JsonIgnoreProperties({"suburb"})を適用しますin theCustomerクラスでは、無視されるフィールドの数に制限はありません。例えば、私は郊外と通りの両方を無視したいです。 @ JsonIgnoreProperties({"suburb"、 "street"})で住所フィールドに注釈を付けます
これをすべて行うことにより、HATEOASタイプのアーキテクチャを作成できます。
以下は完全なコードです
Customer.Java
public class Customer {
private int id;
private String email;
private String name;
@JsonIgnoreProperties({"suburb", "street"})
private Address address;
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Address.Java public class Address {
private String street;
private String suburb;
private String Link link;
public Link getLink() {
return link;
}
public void setLink(Link link) {
this.link = link;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getSuburb() {
return suburb;
}
public void setSuburb(String suburb) {
this.suburb = suburb;
}
}
@ user356083に加えて、@ ResponseBodyが返されたときにこの例が機能するようにいくつかの変更を加えました。これはThreadLocalを使用したちょっとしたハックですが、Springはこれを適切に行うために必要なコンテキストを提供していないようです。
public class ViewThread {
private static final ThreadLocal<Class<?>[]> viewThread = new ThreadLocal<Class<?>[]>();
private static final Log log = LogFactory.getLog(SocialRequestUtils.class);
public static void setKey(Class<?>[] key){
viewThread.set(key);
}
public static Class<?>[] getKey(){
if(viewThread.get() == null)
log.error("Missing threadLocale variable");
return viewThread.get();
}
}
public class JsonViewInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
JsonView jsonViewAnnotation = handlerMethod
.getMethodAnnotation(JsonView.class);
if (jsonViewAnnotation != null)
ViewThread.setKey(jsonViewAnnotation.value());
return true;
}
}
public class MappingJackson2HttpMessageConverter extends
AbstractHttpMessageConverter<Object> {
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
JsonGenerator jsonGenerator =
this.objectMapper.getJsonFactory().createJsonGenerator(outputMessage.getBody(), encoding);
// This is a workaround for the fact JsonGenerators created by ObjectMapper#getJsonFactory
// do not have ObjectMapper serialization features applied.
// See https://github.com/FasterXML/jackson-databind/issues/12
if (objectMapper.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
jsonGenerator.useDefaultPrettyPrinter();
}
//A bit of a hack.
Class<?>[] jsonViews = ViewThread.getKey();
ObjectWriter writer = null;
if(jsonViews != null){
writer = this.objectMapper.writerWithView(jsonViews[0]);
}else{
writer = this.objectMapper.writer();
}
try {
if (this.prefixJson) {
jsonGenerator.writeRaw("{} && ");
}
writer.writeValue(jsonGenerator, object);
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
}
}
4.2.0.Release以降、これは次のように簡単に実行できます。
@RequestMapping(method = RequestMethod.POST, value = "/springjsonfilter")
public @ResponseBody MappingJacksonValue byJsonFilter(...) {
MappingJacksonValue jacksonValue = new MappingJacksonValue(responseObj);
jacksonValue.setFilters(customFilterObj);
return jacksonValue;
}
参照:1. https://jira.spring.io/browse/SPR-12586 2. http://wiki.fasterxml.com/JacksonFeatureJsonFilter