Spring MVC more flexible control of json return issues (custom filter fields)

  • 2020-06-07 04:32:41
  • OfStack

This article focuses on how Spring MVC dynamically returns Json data. When we do Web interface development, we often encounter this scenario.

Two requests that return the same object but require different return fields. As in the following scenario


/**
*  Returns all names as well Id
*/
@RequestMapping("list")
@ResponseBody
public List<Article> findAllNameAndId() {
 return articleService.findAll();
}

/**
*  Returns all catalog details 
*/
@RequestMapping("list-detail")
@ResponseBody
public List<Article> findAllDetail() {
 return articleService.findAll();
}

Spring MVC is jackson by default using the convert json framework. As you know, jackson could annotate the entity class to specify the serialization rules, but that would be inflexible and not achieve the situation we currently want.

This article focuses on custom annotations for more flexibility and fine-grained control over json conversion.

Finally, we need to achieve the following results:


@RequestMapping(value = "{id}", method = RequestMethod.GET)
//  Not included on return  filter  Within the  createTime, updateTime  field 
@JSON(type = Article.class, filter="createTime,updateTime") 
public Article get(@PathVariable String id) {
  return articleService.get(id);
}
@RequestMapping(value="list", method = RequestMethod.GET)
//  Contains only on return  include  Within the  id, name  field  
@JSON(type = Article.class , include="id,name")
public List<Article> findAll() {
  return articleService.findAll();
}

The jackson program filters fields

In jackson, we can add the @JsonFilter annotation to the entity class and set the filter rules through ES30en.setFilterProvider. Here is a brief introduction to the use of setFilterProvider 1


@JsonFilter("ID-TITLE")
class Article {
 private String id;
 private String title;
 private String content;
 // ... getter/setter
}

// Demo
class Demo {
 public void main(String args[]) {
  ObjectMapper mapper = new ObjectMapper();
  // SimpleBeanPropertyFilter.filterOutAllExcept("id,title")
  //  Filter in addition to  id,title  All fields other than that, when serialized, contain only  id  and  title
  mapper.setFilterProvider(new SimpleFilterProvider().addFilter("ID-TITLE",
          SimpleBeanPropertyFilter.filterOutAllExcept("id,title"))); 

  String filterOut = mapper.writeValueAsString(new Article());

  mapper = new ObjectMapper();
  // SimpleBeanPropertyFilter.serializeAllExcept("id,title")
  //  Serialize all fields, but exclude them  id  and  title In other words, in addition to  id  and  title Other fields are included  json
  mapper.setFilterProvider(new SimpleFilterProvider().addFilter("ID-TITLE",
      SimpleBeanPropertyFilter.serializeAllExcept(filter.split("id,title"))));

  String serializeAll = mapper.writeValueAsString(new Article());

  System.out.println("filterOut:" + filterOut);
  System.out.println("serializeAll :" + serializeAll);  
 }
}

The output


filterOut:{id: "", title: ""}
serializeAll:{content:""}

Encapsulates the json transformation

Through the above code, we found that setFilterProvider can be used to flexibly handle fields that need to be filtered. However, there is one drawback to the above approach, which is that the annotations should be added to the original model. Here we use ES45en.addMixIn (Class) < ? > type, Class < ? > mixinType) method, which mixes the annotations of the two classes so that the first parameter class has the annotations of the second parameter class. Decouple the model and @JsonFilter annotations that need to be filtered


package diamond.cms.server.json;

import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

/**
 * depend on jackson
 * @author Diamond
 */
public class CustomerJsonSerializer {

  static final String DYNC_INCLUDE = "DYNC_INCLUDE";
  static final String DYNC_FILTER = "DYNC_FILTER";
  ObjectMapper mapper = new ObjectMapper();

  @JsonFilter(DYNC_FILTER)
  interface DynamicFilter {
  }

  @JsonFilter(DYNC_INCLUDE)
  interface DynamicInclude {
  }

  /**
   * @param clazz  You need to set the rules Class
   * @param include  Which fields are included in the transformation 
   * @param filter  Which fields are filtered when transforming 
   */
  public void filter(Class<?> clazz, String include, String filter) {
    if (clazz == null) return;
    if (include != null && include.length() > 0) {
      mapper.setFilterProvider(new SimpleFilterProvider().addFilter(DYNC_INCLUDE,
          SimpleBeanPropertyFilter.filterOutAllExcept(include.split(","))));
      mapper.addMixIn(clazz, DynamicInclude.class);
    } else if (filter !=null && filter.length() > 0) {
      mapper.setFilterProvider(new SimpleFilterProvider().addFilter(DYNC_FILTER,
          SimpleBeanPropertyFilter.serializeAllExcept(filter.split(","))));
      mapper.addMixIn(clazz, DynamicFilter.class);
    }
  }

  public String toJson(Object object) throws JsonProcessingException {
    return mapper.writeValueAsString(object);
  }
}

Our previous Demo can be:


// Demo
class Demo {
 public void main(String args[]) {
  CustomerJsonSerializer cjs= new CustomerJsonSerializer();
  //  Set the switch  Article  Class, contains only  id, name
  cjs.filter(Article.class, "id,name", null); 

  String include = cjs.toJson(new Article()); 

  cjs = new CustomerJsonSerializer();
  //  Set the switch  Article  Class, filter out  id, name
  cjs.filter(Article.class, null, "id,name"); 

  String filter = cjs.toJson(new Article());

  System.out.println("include: " + include);
  System.out.println("filter: " + filter);  
 }
}

The output


include: {id: "", title: ""}
filter: {content:""}

Custom @ES69en annotation

We need to achieve the effect at the beginning of the article. Here I have a custom annotation that can be added to a method to carry arguments to the CustomerJsonSerializer.filter method, where certain fields of a class need to be filtered or included.


package diamond.cms.server.json;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface JSON {
  Class<?> type();
  String include() default "";
  String filter() default "";
}

HandlerMethodReturnValueHandler for Spring MVC

The HandlerMethodReturnValueHandler interface Spring MVC is used to handle request return values. The interface has two methods, supportsReturnType, to determine whether the processing class supports the current request. handleReturnValue is the implementation of the return logic.


 // Spring MVC  The source code 
package org.springframework.web.method.support;

import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;

public interface HandlerMethodReturnValueHandler {

  boolean supportsReturnType(MethodParameter returnType);

  void handleReturnValue(Object returnValue, MethodParameter returnType,
      ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;

}

The @ResponseBody class is the one we give to RequestResponseBodyMethodProcessor

And when we return ModelAndView, it's handled by the ModelAndViewMethodReturnValueHandler class

To implement the effect at the beginning of this article, I implemented an JsonReturnHandler class that handles return values when a method has an @JSON annotation.


package diamond.cms.server.json.spring;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import diamond.cms.server.json.CustomerJsonSerializer;
import diamond.cms.server.json.JSON;

public class JsonReturnHandler implements HandlerMethodReturnValueHandler{

  @Override
  public boolean supportsReturnType(MethodParameter returnType) { 
    //  If we have our own custom  JSON  annotations   Let's use this one Handler  To deal with 
    boolean hasJsonAnno= returnType.getMethodAnnotation(JSON.class) != null;
    return hasJsonAnno;
  }

  @Override
  public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest) throws Exception {
    //  I'm going to set this to be the final processing class, and I'm not going to look for it 1 Three classes for processing 
    mavContainer.setRequestHandled(true);

    //  Get the annotation and execute it filter methods   The last return 
    HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
    Annotation[] annos = returnType.getMethodAnnotations();
    CustomerJsonSerializer jsonSerializer = new CustomerJsonSerializer();
    Arrays.asList(annos).forEach(a -> {
      if (a instanceof JSON) {
        JSON json = (JSON) a;
        jsonSerializer.filter(json.type(), json.include(), json.filter());
      }
    });

    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
    String json = jsonSerializer.toJson(returnValue);
    response.getWriter().write(json);
  }
}

With this, we can finally achieve the following effects.


@RequestMapping(value = "{id}", method = RequestMethod.GET)
//  Not included on return  filter  Within the  createTime, updateTime  field 
@JSON(type = Article.class, filter="createTime,updateTime") 
public Article get(@PathVariable String id) {
  return articleService.get(id);
}
@RequestMapping(value="list", method = RequestMethod.GET)
//  Contains only on return  include  Within the  id, name  field  
@JSON(type = Article.class , include="id,name")
public List<Article> findAll() {
  return articleService.findAll();
}
0

Request/article / {articleId}


@RequestMapping(value = "{id}", method = RequestMethod.GET)
//  Not included on return  filter  Within the  createTime, updateTime  field 
@JSON(type = Article.class, filter="createTime,updateTime") 
public Article get(@PathVariable String id) {
  return articleService.get(id);
}
@RequestMapping(value="list", method = RequestMethod.GET)
//  Contains only on return  include  Within the  id, name  field  
@JSON(type = Article.class , include="id,name")
public List<Article> findAll() {
  return articleService.findAll();
}
1

Request article/list


@RequestMapping(value = "{id}", method = RequestMethod.GET)
//  Not included on return  filter  Within the  createTime, updateTime  field 
@JSON(type = Article.class, filter="createTime,updateTime") 
public Article get(@PathVariable String id) {
  return articleService.get(id);
}
@RequestMapping(value="list", method = RequestMethod.GET)
//  Contains only on return  include  Within the  id, name  field  
@JSON(type = Article.class , include="id,name")
public List<Article> findAll() {
  return articleService.findAll();
}
2

Download: cms-ES119en-ES120en_ES121en51.rar


Related articles: