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