开发者

Jackson's @JsonView, @JsonFilter and Spring

开发者 https://www.devze.com 2023-04-10 14:04 出处:网络
Can one use the Jackson @JsonView and @JsonFilter annotations to modify the JSON returned by a Spring MVC controller, whilst using MappingJacksonHttpMessageConverterand Spring\'s @ResponseBody and @Re

Can one use the Jackson @JsonView and @JsonFilter annotations to modify the JSON returned by a Spring MVC controller, whilst using MappingJacksonHttpMessageConverterand Spring's @ResponseBody and @RequestBody annotations?

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;
    ...
}

When the client requests a collection of Products, I'd like to return a minimal version of each ProductDescription, perhaps just its ID. Then in a subsequent call the client can use this ID to ask for a full in开发者_运维知识库stance of ProductDescription with all properties present.

It would be ideal to be able to specify this on the Spring MVC controller methods, as the method invoked defines the context in which client was requesting the data.


This issue is solved!
Follow this

Add support for Jackson serialization views

Spring MVC now supports Jackon's serialization views for rendering different subsets of the same POJO from different controller methods (e.g. detailed page vs summary view). Issue: SPR-7156

This is the SPR-7156.

Status: Resolved

Description

Jackson's JSONView annotation allows the developer to control which aspects of a method are serialiazed. With the current implementation, the Jackson view writer must be used but then the content type is not available. It would be better if as part of the RequestBody annotation, a JSONView could be specified.

Available on Spring ver >= 4.1

UPDATE

Follow this link. Explains with an example the @JsonView annotation.


Ultimately, we want to use notation similar to what StaxMan showed for JAX-RS. Unfortunately, Spring doesn't support this out of the box, so we have to do it ourselves.

This is my solution, it's not very pretty, but it does the job.

@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());
    }
}

In 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>

and

<mvc:interceptors>
    <bean class="com.mycompany.myproject.JsonViewInterceptor" />
</mvc:interceptors>


I don't know how things work with Spring (sorry!), but Jackson 1.9 can use @JsonView annotation from JAX-RS methods, so you can do:

@JsonView(ViewId.class)
@GET // and other JAX-RS annotations
public Pojo resourceMethod()
{
   return new Pojo();
} 

and Jackson will use View identified by ViewId.class as the active view. Perhaps Spring has (or will have) similar capability? With JAX-RS this is handled by standard JacksonJaxrsProvider, for what that's worth.


Looking for the same answer I came up with an idea to wrap ResponseBody object with a view.

Piece of controller class:

@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;
}

}


Then I override writeInternal method form MappingJackson2HttpMessageConverter to check if object to serialize is instanceof wrapper, if so I serialize object with required view.

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);
}

}


The answer to this after a many head banging dead ends and nerd rage tantrums is.... so simple. In this use case we have a Customer bean with a complex object Address embedded within it and we want to prevent the serialization of a property name surburb and street in address, when the json serialization take place.

We do this by applying an annotation @JsonIgnoreProperties({"suburb"}) on the field address in the Customer class, the number of fields to be ignored is limitless. e.g i want to ingnore both suburb and street. I would annotate the address field with @JsonIgnoreProperties({"suburb", "street"})

By doing all this we can create HATEOAS type architecture.

Below is the full code

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;
}

}


In addition to @user356083 I've made some modifications to make this example work when a @ResponseBody is returned. It's a bit of a hack using ThreadLocal but Spring doesn't seem to provide the necessary context to do this the nice way.

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); 
        } 
    }


Even better, since 4.2.0.Release, you can do this simply as follows:

@RequestMapping(method = RequestMethod.POST, value = "/springjsonfilter")
    public @ResponseBody MappingJacksonValue byJsonFilter(...) {
        MappingJacksonValue jacksonValue = new MappingJacksonValue(responseObj);    
        jacksonValue.setFilters(customFilterObj);
        return jacksonValue;
    }

References: 1. https://jira.spring.io/browse/SPR-12586 2. http://wiki.fasterxml.com/JacksonFeatureJsonFilter

0

精彩评论

暂无评论...
验证码 换一张
取 消