Spring MVC 更灵活的控制 json 返回(自定义过滤字段)
Spring MVC 更灵活的控制 json 返回(自定义过滤字段)
DiamondFsd 发表于9个月前
Spring MVC 更灵活的控制 json 返回(自定义过滤字段)
  • 发表于 9个月前
  • 阅读 9499
  • 收藏 648
  • 点赞 31
  • 评论 83

腾讯云 学生专属云服务套餐 10元起购>>>   

这篇文章主要讲 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 注解,如果是嵌套对象的话
@JSON(type = Article.class  , include="id,name,createTime")
@JSON(type = Tag.class, include="id,name")
public List<Article> findAll() {
    return articleService.findAll();
}

jackson 编程式过滤字段

jackson 中, 我们可以通过 ObjectMapper.setFilterProvider 来进行过滤规则的设置,jackson 内置了一个 SimpleFilterProvider 过滤器,这个过滤器功能比较单一,不能很好的支持我们想要的效果。于是我自己实现了一个过滤器 JacksonJsonFilter

package diamond.cms.server.json;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.BeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.PropertyFilter;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;

@SuppressWarnings("deprecation")
@JsonFilter("JacksonFilter")
public class JacksonJsonFilter extends FilterProvider{

    Map<Class<?>, Set<String>> includeMap = new HashMap<>();
    Map<Class<?>, Set<String>> filterMap = new HashMap<>();

    public void include(Class<?> type, String[] fields) {
        addToMap(includeMap, type, fields);
    }

    public void filter(Class<?> type, String[] fields) {
        addToMap(filterMap, type, fields);
    }

    private void addToMap(Map<Class<?>, Set<String>> map, Class<?> type, String[] fields) {
        Set<String> fieldSet = map.getOrDefault(type, new HashSet<>());
        fieldSet.addAll(Arrays.asList(fields));
        map.put(type, fieldSet);
    }

    @Override
    public BeanPropertyFilter findFilter(Object filterId) {
        throw new UnsupportedOperationException("Access to deprecated filters not supported");
    }

    @Override
    public PropertyFilter findPropertyFilter(Object filterId, Object valueToFilter) {

        return new SimpleBeanPropertyFilter() {

            @Override
            public void serializeAsField(Object pojo, JsonGenerator jgen, SerializerProvider prov, PropertyWriter writer)
                    throws Exception {
                if (apply(pojo.getClass(), writer.getName())) {
                    writer.serializeAsField(pojo, jgen, prov);
                } else if (!jgen.canOmitFields()) {
                    writer.serializeAsOmittedField(pojo, jgen, prov);
                }
            }
        };
    }

    public boolean apply(Class<?> type, String name) {
        Set<String> includeFields = includeMap.get(type);
        Set<String> filterFields = filterMap.get(type);
        if (includeFields != null && includeFields.contains(name)) {
            return true;
        } else if (filterFields != null && !filterFields.contains(name)) {
            return true;
        } else if (includeFields == null && filterFields == null) {
            return true;
        }
        return false;
    }

}

通过这个过滤器,我们可以实现

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();
    JacksonJsonFilter jacksonFilter = new JacksonJsonFilter();
 
    // 过滤除了 id,title 以外的所有字段,也就是序列化的时候,只包含 id 和 title
    jacksonFilter.include(Article.class, "id,title");
    mapper.setFilterProvider(jacksonFilter);  // 设置过滤器
    mapper.addMixIn(Article.class, jacksonFilter.getClass()); // 为Article.class类应用过滤器
    String include= mapper.writeValueAsString(new Article());
    
    
    // 序列化所有字段,但是排除 id 和 title,也就是除了 id 和 title之外,其他字段都包含进 json
    jacksonFilter = new JacksonJsonFilter();
    jacksonFilter.filter(Article.class, "id,title");
    mapper = new ObjectMapper();
    mapper.setFilterProvider(jacksonFilter);
    mapper.addMixIn(Article.class, jacksonFilter.getClass()); 
    
    String filter = mapper.writeValueAsString(new Article());
     
    System.out.println("include:" + include);
    System.out.println("filter :" + filter);   
  }
}

输出结果
filterOut:{id: "", title: ""}
serializeAll:{content:""}

自定义 @JSON 注解

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

package diamond.cms.server.json;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(JSONS.class)   // 让方法支持多重@JSON 注解
public @interface JSON {
    Class<?> type();
    String include() default "";
    String filter() default "";
}

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 JSONS {
    JSON [] value();
}

封装 JSON 转换

注解有了,过滤器也有了,那么我们来封装一个类,用作解析注解以及设置过滤器的。 CustomerJsonSerializer.java

package diamond.cms.server.json;

import org.apache.commons.lang3.StringUtils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

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


    ObjectMapper mapper = new ObjectMapper();
    JacksonJsonFilter jacksonFilter = new JacksonJsonFilter();

    /**
     * @param clazz target type
     * @param include include fields
     * @param filter filter fields
     */
    public void filter(Class<?> clazz, String include, String filter) {
        if (clazz == null) return;
        if (StringUtils.isNotBlank(include)) {
            jacksonFilter.include(clazz, include.split(","));
        }
        if (StringUtils.isNotBlank(filter)) {
            jacksonFilter.filter(clazz, filter.split(","));
        }
        mapper.addMixIn(clazz, jacksonFilter.getClass());
    }

    public String toJson(Object object) throws JsonProcessingException {
        mapper.setFilterProvider(jacksonFilter);
        return mapper.writeValueAsString(object);
    }
    public void filter(JSON json) {
        this.filter(json.type(), json.include(), json.filter());
    }
}

我们之前的 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:""}

实现 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);
            } else if (a instanceof JSONS) { // 使用多重注解时,实际返回的是 @Repeatable(JSONS.class) 内指定的 @JSONS 注解
                JSONS jsons = (JSONS) a;
                Arrays.asList(jsons.value()).forEach(json -> {
                    jsonSerializer.filter(json);
                });
            }
        });

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

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

class Tag {
  private String id;
  private String tagName;
}
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")
  @JSON(type = Tag.class, filter="id")
  public List<Article> findAll() {
      return articleService.findAll();
  }
}

请求 /article/{articleId}

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

请求 article/list

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

博客源码

以上就是这篇教程的全部内容了。 我博客系统的后台,就是使用的这种方式来 自定义返回字段的。
上面这些代码都是为了写教程有一定的精简, 完整的可以看 github 上的源码
Blog-End-Json-Serializer 序列化的部分
Blog-End-Full-Code 整个博客后台的代码


个人博客地址

共有 人打赏支持
粉丝 84
博文 15
码字总数 16372
评论 (83)
咖啡碼農
依然不满足很多场景,比如,有些字段的返回是前端调用接口时想自由控制的,通过传递需要返回的属性来获取相应的值。
DiamondFsd

引用来自“咖啡碼農”的评论

依然不满足很多场景,比如,有些字段的返回是前端调用接口时想自由控制的,通过传递需要返回的属性来获取相应的值。
你这个属于具体的业务逻辑了。其实也是可以封装一下,就是在 `handleReturnValue` 中获取需要返回的字段,然后序列化返回就好了
超级奶爸老谭

引用来自“咖啡碼農”的评论

依然不满足很多场景,比如,有些字段的返回是前端调用接口时想自由控制的,通过传递需要返回的属性来获取相应的值。
修改一下代码,把需要返回的字段放在请求的headers里面也可以实现,感谢博主分享!
MGL_ONE
返回类型 直接jsonobject 不行?
DiamondFsd

引用来自“MGL_ONE”的评论

返回类型 直接jsonobject 不行?
比如呢?有例子么
HeyS1
配置一下配置文件,如果为null就不返回。
DiamondFsd

引用来自“月破轻云”的评论

配置一下配置文件,如果为null就不返回。
有时候查询出来的数据不为空,但是也不想让它返回。
当然,如果你能做到查询语句和需要的字段同步的话,可以直接配置非空返回
TiMoLove
感觉用JsonView注解能满足很多场景了,格式很不统一的情况下另写DTO。
(其实特别想要一种,只需要一次指定,就能自动指定数据库查询字段和返回字段的东西)
DiamondFsd

引用来自“TiMoLove”的评论

感觉用JsonView注解能满足很多场景了,格式很不统一的情况下另写DTO。
(其实特别想要一种,只需要一次指定,就能自动指定数据库查询字段和返回字段的东西)
因为JsonView 需要在model上加注解,而在我的环境中,model都是自动生成的。加了注解后再生成,就又要重写一遍,所以最后选择了这个方案。
至于你说的场景,就需要看你使用的ORM来确定复杂程度了。
如果你使用的是 mybatis,因为sql都是写在xml里面的,动态查询字段实现起来就比较麻烦。
如果是其他编程式的ORM,就可以根据一定的规则,来确定查询字段以及返回内容。单具体的实现还是要跟具体的业务场景和你整体架构来决定的
JeffreyLin
jstl view 用这个标签库也不错:
<groupId>atg.taglib.json</groupId>
<artifactId>json-taglib</artifactId>
<version>0.4.1</version>
陌路千里
定义VO好了
DiamondFsd

引用来自“陌路千里”的评论

定义VO好了
这也是一种方法,不过当一个model 有 10个接口需要不同的json返回值的时候,定义VO就相对来说比较麻烦了
ODMark
设计上参考GraphQL的思想的话,可能会更灵活好用。
优秀良民
spring boot 如何去使用 HandlerMethodReturnValueHandler接口啊,继承HandlerMethodReturnValueHandler或是WebMvcConfigurerAdapter,去重写addReturnValueHandlers方法,都无效。。。
DiamondFsd

引用来自“优秀良民”的评论

spring boot 如何去使用 HandlerMethodReturnValueHandler接口啊,继承HandlerMethodReturnValueHandler或是WebMvcConfigurerAdapter,去重写addReturnValueHandlers方法,都无效。。。
我用的是addReturnValueHandlers 可以看一下github上的 https://github.com/k55k32/cms-admin-end/blob/master/src/main/java/diamond/cms/server/config/WebConfig.java#L42

不过我这边有做一个特殊处理,因为我用的都是 @RestController 注解所以所有方法都会默认加上 @ResposneBody。
而且在 Spring MVC 中 自定义的 HandlerMethodReturnValueHandler 会排在自带的后面,也就是Spring用了@ResponseBody的处理类,就忽略了我们自身定义到 处理类,所以我们需要将自己的 HandlerMethodReturnValueHandler 实现类顺序提到前面。


可以看一下 https://github.com/k55k32/cms-admin-end/blob/master/src/main/java/diamond/cms/server/json/spring/JsonReturnHandler.java 我这个handler的源码,我同时也实现了 BeanPostProcessor 接口,并且将本身handler的顺序排到了第一位
Spring-JPA
数据库查询直接返回Map, 要什么字段,就查询什么字段
DiamondFsd

引用来自“Spring-JPA”的评论

数据库查询直接返回Map, 要什么字段,就查询什么字段
如果能控制所有的查询语句返回对应的字段,是可以的。
但是数据库直接返回Map,这样的方式可读性就差了很多
123咔哒
github地址是什么?
DiamondFsd

引用来自“123咔哒”的评论

github地址是什么?
文章尾部有
TiMoLove

引用来自“MGL_ONE”的评论

返回类型 直接jsonobject 不行?
JsonObject说白了就是个Map,不是特殊场景还是不要用吧?
×
DiamondFsd
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: