Spring MVC 更灵活的控制 json 返回问题(自定义过滤字段)

这篇文章主要讲 Spring MVC 如何动态的去返回 Json 数据 在我们做 Web 接口开发的时候, 经常会遇到这种场景。

两个请求,返回同一个对象,但是需要的返回字段并不相同。如以下场景

/**
* 返回所有名称以及Id
*/
@RequestMapping("list")
@ResponseBody
public List<Article> findAllNameAndId() {
 return articleService.findAll();
}

/**
* 返回所有目录详情
*/
@RequestMapping("list-detail")
@ResponseBody
public List<Article> findAllDetail() {
 return articleService.findAll();
}

Spring MVC 默认使用转json框架是 jackson。 大家也知道, jackson 可以在实体类内加注解,来指定序列化规则,但是那样比较不灵活,不能实现我们目前想要达到的这种情况。

这篇文章主要讲的就是通过自定义注解,来更加灵活,细粒化控制 json 格式的转换。

最终我们需要实现如下的效果:

@RequestMapping(value = "{id}", method = RequestMethod.GET)
// 返回时候不包含 filter 内的 createTime, updateTime 字段
@JSON(type = Article.class, filter="createTime,updateTime") 
public Article get(@PathVariable String id) {
  return articleService.get(id);
}
@RequestMapping(value="list", method = RequestMethod.GET)
// 返回时只包含 include 内的 id, name 字段 
@JSON(type = Article.class , include="id,name")
public List<Article> findAll() {
  return articleService.findAll();
}

jackson 编程式过滤字段

jackson 中, 我们可以在实体类上加上 @JsonFilter 注解,并且通过 ObjectMapper.setFilterProvider 来进行过滤规则的设置。 这里简单介绍一下 setFilterProvider 的使用

@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")
  // 过滤除了 id,title 以外的所有字段,也就是序列化的时候,只包含 id 和 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")
  // 序列化所有字段,但是排除 id 和 title,也就是除了 id 和 title之外,其他字段都包含进 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);  
 }
}

输出结果

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

封装json转换

通过上面的代码,我们发现,可以使用 setFilterProvider 来灵活的处理需要过滤的字段。不过上面的方法还有一些缺陷就是,还是要在 原来的 model 上加注解,这里我们使用 ObjectMapper.addMixIn(Class<?> type, Class<?> mixinType) 方法,这个方法就是讲两个类的注解混合,让第一个参数的类能够拥有第二个参数类的注解。让需要过滤的 model 和 @JsonFilter 注解解除耦合

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 需要设置规则的Class
   * @param include 转换时包含哪些字段
   * @param filter 转换时过滤哪些字段
   */
  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);
  }
}

我们之前的 Demo 可以变成:

// Demo
class Demo {
 public void main(String args[]) {
  CustomerJsonSerializer cjs= new CustomerJsonSerializer();
  // 设置转换 Article 类时,只包含 id, name
  cjs.filter(Article.class, "id,name", null); 

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

  cjs = new CustomerJsonSerializer();
  // 设置转换 Article 类时,过滤掉 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);  
 }
}

输出结果

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

自定义 @JSON 注解

我们需要实现文章开头的那种效果。这里我自定义了一个注解,可以加在方法上,这个注解是用来携带参数给 CustomerJsonSerializer.filter 方法的,就是某个类的某些字段需要过滤或者包含。

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

实现 Spring MVC 的 HandlerMethodReturnValueHandler

HandlerMethodReturnValueHandler 接口 Spring MVC 用于处理请求返回值 。 看一下这个接口的定义和描述,接口有两个方法supportsReturnType 用来判断 处理类 是否支持当前请求, handleReturnValue 就是具体返回逻辑的实现。

 // Spring MVC 源码
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;

}

我们平时使用 @ResponseBody 就是交给 RequestResponseBodyMethodProcessor 这个类处理的

还有我们返回 ModelAndView 的时候, 是由 ModelAndViewMethodReturnValueHandler 类处理的

要实现文章开头的效果,我实现了一个 JsonReturnHandler类,当方法有 @JSON 注解的时候,使用该类来处理返回值。

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) { 
    // 如果有我们自定义的 JSON 注解 就用我们这个Handler 来处理
    boolean hasJsonAnno= returnType.getMethodAnnotation(JSON.class) != null;
    return hasJsonAnno;
  }

  @Override
  public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest) throws Exception {
    // 设置这个就是最终的处理类了,处理完不再去找下一个类进行处理
    mavContainer.setRequestHandled(true);

    // 获得注解并执行filter方法 最后返回
    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);
  }
}

通过这些,我们就可以最终实现以下效果。

class Article {
 private String id;
 private String title;
 private String content;
 private Long createTime;
 // ... getter/setter
}

@Controller
@RequestMapping("article")
class ArticleController {
 @RequestMapping(value = "{id}", method = RequestMethod.GET)
 @JSON(type = Article.class, filter="createTime") 
 public Article get(@PathVariable String id) {
   return articleService.get(id);
 }

 @RequestMapping(value="list", method = RequestMethod.GET)
 @JSON(type = Article.class , include="id,title")
 public List<Article> findAll() {
   return articleService.findAll();
 }
}

请求 /article/{articleId}

{
  id: "xxxx",
  title: "xxxx",
  content: "xxxx"
}

请求 article/list

[ {id: "xx", title: ""}, {id: "xx", title: ""}, {id: "xx", title: ""} ... ]

下载地址:cms-admin-end_jb51.rar

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持菜鸟教程(cainiaojc.com)。

声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:notice#cainiaojc.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。