概述
spring/spring boot 返回的json数据,通常包含了对象所有的字段,有时候浪费流量。例如一个接口有10个字段,而前端只需要2个字段,都返回会浪费流量。
解决方案:前端在header中传递需要包含或需要排除的字段;后端在返回数据前进行统一拦截,只返回需要的字段。
具有有多种实现方式(这里只提供spring boot)。
首先约定返回的BaseResult对象格式如下,里面result属性就是实际各种数据对象。
{
"ret":0,
"msg":null,
"result":{
"id":1,
"name":"后摄像头53"
},
"time":1540972430498
}
实现方式一:通过AOP controller来实现
aop实现步骤说明:
- 判断返回的是不是BaseResult对象
- 判断request header或params是否有x-include-fields、x-exclude-fields属性(有则取出来放入set中)
- 满足以上条件则对BaseResult.result 对象进行处理,用Map替换result对象,Map只返回需要的字段。如果是Array或Collection则每个Item替换成一个Map。
import com.cehome.cloudbox.common.object.BaseResult;
import com.cehome.cloudbox.common.object.ItemsResult;
import com.cehome.cloudbox.common.object.PageResult;
import com.cehome.cloudbox.common.page.Page;
import com.cehomex.spring.feign.FeignRequestHolder;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.beans.PropertyDescriptor;
import java.util.*;
@Aspect
public class ControllerAOP {
private static String INCLUDE_FIELDS = "x-include-fields";
private static String EXCLUDE_FIELDS = "x-exclude-fields";
private static String P_INCLUDE_FIELDS = "x-include-fields";
private static String P_EXCLUDE_FIELDS = "x-exclude-fields";
private static final Logger logger = LoggerFactory.getLogger(ControllerAOP.class);
@Pointcut("within(@org.springframework.stereotype.Controller *)")
public void controller() {
}
@Pointcut("within(@org.springframework.web.bind.annotation.RestController *)")
public void restController() {
}
@Around("(controller() || restController()) && execution(public * *(..))")
public Object proceed(ProceedingJoinPoint joinPoint) throws Throwable {
try {
Object object=joinPoint.proceed();
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
if (requestAttributes != null) {
HttpServletRequest request = requestAttributes.getRequest();
handleReturnValue(object,request);
}
return object;
} finally {
FeignRequestHolder.removeAll();
}
}
/**
* 返回前端需要的字段
* @param o
* @param request
* @throws Exception
*/
public void handleReturnValue(Object o,HttpServletRequest request) throws Exception {
if(!isSuccess(o)) return;
//HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
//HttpServletResponse response = nativeWebRequest.getNativeResponse(HttpServletResponse.class);
String fields1 = StringUtils.trimToEmpty(request.getHeader(INCLUDE_FIELDS));
if(fields1.length()==0) fields1 = StringUtils.trimToEmpty(request.getParameter(P_INCLUDE_FIELDS));
String fields2 = StringUtils.trimToEmpty(request.getHeader(EXCLUDE_FIELDS));
if(fields2.length()==0) fields2 = StringUtils.trimToEmpty(request.getParameter(P_EXCLUDE_FIELDS));
if (fields1.length() > 0 || fields2.length() > 0) {
Set<String> includes = fields1.length() == 0 ? new HashSet<>() : new HashSet<>(Arrays.asList(fields1.split(",")));
Set<String> excludes = fields2.length() == 0 ? new HashSet<>() : new HashSet<>(Arrays.asList(fields2.split(",")));
if (o instanceof BaseResult) {
BaseResult result = (BaseResult) o;
Object object = result.getResult();
result.setResult(convertResult(object, includes, excludes));
} else if (o instanceof ItemsResult) {
ItemsResult result = (ItemsResult) o;
Object object = result.getItems();
result.setItems(convertResult(object, includes, excludes));
} else if (o instanceof PageResult) {
PageResult result = (PageResult) o;
Object object = result.getPage();
if (object instanceof Page) {
Page page=(Page) object;
List datas = page.getDatas();
page.setDatas((List)convertResult(datas, includes, excludes));
}
}
}
}
private boolean isSuccess(Object object){
if(object==null) return false;
if (object instanceof BaseResult) return ( (BaseResult) object).isSuccess();
if (object instanceof ItemsResult) return ( (ItemsResult) object).isSuccess();
if (object instanceof PageResult) return ( (PageResult) object).isSuccess();
return false;
}
/*private void handleObject(Object object, Set<String> includes, Set<String> excludes) throws Exception {
PropertyDescriptor[] pds = PropertyUtils.getPropertyDescriptors(object);
for (PropertyDescriptor pd : pds) {
String name = pd.getName();
if (name.equals("class")) {
continue;
}
if (excludes.contains(name) || !includes.contains(name)) {
PropertyUtils.setProperty(object, name, null);
}
}
}*/
/**
* convert objects to maps
* @param object
* @param includes
* @param excludes
* @return
* @throws Exception
*/
private Object convertResult(Object object, Set<String> includes, Set<String> excludes) throws Exception{
if (object instanceof Object[]) {
Object[] objects = (Object[]) object;
return convertArray(objects,includes,excludes);
} else if (object instanceof Collection) {
Collection collection = (Collection) object;
return convertCollection(collection,includes,excludes);
}else{
return convertObject(object,includes,excludes);
}
}
private Collection<Map> convertCollection(Collection collection, Set<String> includes, Set<String> excludes) throws Exception{
Collection<Map> result=new ArrayList<>();
for (Object item : collection) {
result.add(convertObject(item,includes,excludes));
}
return result;
}
private Map[] convertArray(Object[] objects, Set<String> includes, Set<String> excludes) throws Exception{
Map[] result=new HashMap[objects.length];
for(int i=0;i<objects.length;i++){
result[i]=convertObject(objects[i],includes,excludes);
}
return result;
}
/**
* convert object to map
* @param object input
* @param includes include props
* @param excludes exclude props
* @return
* @throws Exception
*/
private Map convertObject(Object object, Set<String> includes, Set<String> excludes) throws Exception {
Map<Object,Object> result=new HashMap<>();
if(!(object instanceof Map)) {
PropertyDescriptor[] pds = PropertyUtils.getPropertyDescriptors(object);
for (PropertyDescriptor pd : pds) {
String name = pd.getName();
if (name.equals("class")) {
continue;
}
if(!excludes.isEmpty() && excludes.contains(name)){
continue;
}
if(!includes.isEmpty() && !includes.contains(name)){
continue;
}
result.put(name,PropertyUtils.getProperty(object, name));
}
}else {
Map<Object,Object> map=(Map<Object,Object>) object;
for(Map.Entry<Object,Object> entry :map.entrySet()){
String name= entry.getKey()==null?"":entry.getKey().toString();
if(!excludes.isEmpty() && excludes.contains(name)){
continue;
}
if(!includes.isEmpty() && !includes.contains(name)){
continue;
}
result.put(entry.getKey(),entry.getValue());
}
}
return result;
}
}
用Map替换的方式改变了原来的对象,还有一种效率更好的不改变对象的方式,
就是把不需要返回的字段设为null,然后配置一个JSON处理bean,统一过滤null的字段。这种方式null的字段就不再返回前端,需要前端做些兼容。
@Bean
@Primary
@ConditionalOnMissingBean(ObjectMapper.class)
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return objectMapper;
}
实现方式二:自定义HttpMessageConverter来实现
spring boot缺省包含了好几个消息转换器,根据返回媒体类型进行匹配,第一个匹配上就忽略掉其它的了。
MappingJackson2HttpMessageConverter 是其处理JSON的消息转换器。
- 所以,需要先删除缺省的MappingJackson2HttpMessageConverter
- 继承MappingJackson2HttpMessageConverter,实现自定义的消息转换。
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//-- 移除缺省的JSON处理器
for (int i = converters.size() - 1; i >= 0; i--) {
HttpMessageConverter<?> messageConverter = converters.get(i);
if (messageConverter instanceof org.springframework.http.converter.json.MappingJackson2HttpMessageConverter)
converters.remove(i);
}
// -- 添加自己得JSON处理器
MappingJackson2HttpMessageConverter c = new MappingJackson2HttpMessageConverter() {
@Override
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
//-- 例子一: 转成fastjson对象,然后替换name字段
JSONObject json = (JSONObject) JSON.toJSON(object);
json.getJSONObject("result").put("name", "coolma");
super.writeInternal(json, type, outputMessage);
//-- 例子二: 用过滤器只保留name字段,其它字段不要。
//注意,例子二需要给BaseResult对象的result属性加上com.fasterxml.jackson.annotation.JsonFilter注解:
// @JsonFilter("result")
// private T result;
MappingJacksonValue value = new MappingJacksonValue(object);
value.setFilters(new SimpleFilterProvider().addFilter("result",
SimpleBeanPropertyFilter.filterOutAllExcept("name")));
super.writeInternal(value, type, outputMessage);
}
};
c.setDefaultCharset(Charset.forName("UTF-8"));
List<MediaType> mediaTypes = new ArrayList<>();
mediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
c.setSupportedMediaTypes(mediaTypes);
converters.add(c);
}
}