基于restful-api接口如何对返回json数据个性化输出

一:应用背景

在介绍功能之前,先说一下工作中遇到的问题。项目中服务端提供restful api接口给前端网站、h5和app端使用,通过http请求返回json数据。目前存在一个A接口,因前期业务需要输出50个业务属性供app端业务开发,现在h5也有相似需求需要用到A接口,不同的是仅用到30个属性就能满足需求了,但是每次请求都返回50个属性。于是前端同学就反馈能否动态指定返回属性呢?针对这个问题私下思考后觉得很有意思,因为如果能实现动态返回,那么对前端开发和数据传输都会带来好处,于是就着手研究了起来……

二:实现思路

我们工程使用spring boot框架,所以第一想到的去看spring boot框架代码,重点看对数据返回前做了啥操作?是否有现成切入口可变更返回值?经过跟踪源代码和百度协助,有2种方式可实现。

2.1.基于ResponseBodyAdvice特性方式实现。此方式最简单、方便,推荐使用。

2.2.继承MappingJackson2HttpMessageConverter,重写writeInternal方法方式实现。此方式比较复杂,中间还要重写BeanSerializerFactory、BeanSerializerBuilder等对象。不推荐使用。

三:代码实现

3.1 基于ResponseBodyAdvice特性方式实现

首先我们一起看一下这个接口源代码:

public interface ResponseBodyAdvice<T> {

	/**
	 * Whether this component supports the given controller method return type
	 * and the selected {@code HttpMessageConverter} type.
	 * @param returnType the return type
	 * @param converterType the selected converter type
	 * @return {@code true} if {@link #beforeBodyWrite} should be invoked;
	 * {@code false} otherwise
	 */
	boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

	/**
	 * Invoked after an {@code HttpMessageConverter} is selected and just before
	 * its write method is invoked.
	 * @param body the body to be written
	 * @param returnType the return type of the controller method
	 * @param selectedContentType the content type selected through content negotiation
	 * @param selectedConverterType the converter type selected to write to the response
	 * @param request the current request
	 * @param response the current response
	 * @return the body that was passed in or a modified (possibly new) instance
	 */
	T beforeBodyWrite(T body, MethodParameter returnType, MediaType selectedContentType,
			Class<? extends HttpMessageConverter<?>> selectedConverterType,
			ServerHttpRequest request, ServerHttpResponse response);

}

从上面源代码我们可以看出,这个接口非常简单,就提供了2个接口方法:supports和beforeBodyWrite。

supports方法:可以根据MethodParameter和Class反射类名称和方法名称,判断是否执行beforeBodyWrite方法。ture是执行beforeBodyWrite方法,false不执行。

beforeBodyWrite方法:在Controller方法执行完毕后,并且在序列化之前可以对返回值对象做加工处理。参数body就是实际返回值对象。

根据上面说明只需实现这个接口重写它的beforeBodyWrite方法即可。目标就是对body对象参数加工处理,来实现我们的最终需求:

package com.example.demo.advice;

import java.util.List;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.StringUtils;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import com.example.demo.util.Helper;

import net.sf.json.JSONArray;
import net.sf.json.JSONObject;

/**
 * 对返回对象个性化过滤处理
 *
 * @author 吴敏强
 * @since 1.0
 */
@Order(1)
@ControllerAdvice(basePackages = "com.example.demo.controller")
public class MyResponseBodyAdvice implements ResponseBodyAdvice {
    // 包含项
    private String[] includes = {};
    // 排除项
    private String[] excludes = {};

    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        // 这里可以根据自己的需求
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass,
            ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        // 重新初始化为默认值
        includes = new String[] {};
        excludes = new String[] {};

        // 判断返回的对象是单个对象,还是list,活着是map
        if (o == null) {
            return null;
        }

        // 通过 ServerHttpRequest的实现类ServletServerHttpRequest 获得HttpServletRequest
        ServletServerHttpRequest sshr = (ServletServerHttpRequest) serverHttpRequest;
        // 此处获取到request
        HttpServletRequest request = sshr.getServletRequest();

        String includes_str = (String) request.getAttribute("includes");
        String excludes_str = (String) request.getAttribute("excludes");
        if (StringUtils.isNotBlank(includes_str)) {
            includes = includes_str.split(",");
        }
        if (StringUtils.isNotBlank(excludes_str)) {
            excludes = excludes_str.split(",");
        }

        if (includes.length == 0 && excludes.length == 0) {
            return o;
        }

        try {
            if (o instanceof List) {
                JSONArray json = JSONArray.fromObject(o);
                Helper.converterResponseBody(json, includes, excludes);
                return json;
            } else {
                JSONObject json = JSONObject.fromObject(o);
                Helper.converterResponseBody(json, includes, excludes);
                return json;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return o;
    }
}

其中converterResponseBody方法就是最终对返回值的处理逻辑,主要使用一个递归方式循环遍历返回值然后做相应处理。

public static boolean converterResponseBody(Object expect_json, String[] includes, String[] excludes) {
        boolean isEmptyObject = false;
        if (expect_json instanceof JSONArray) {
            JSONArray objArray = (JSONArray) expect_json;
            List<Integer> dellist = new ArrayList<>();
            for (int i = 0; i < objArray.size(); i++) {
                boolean ep = Helper.converterResponseBody(objArray.get(i), includes, excludes);
                // 如果是空数组,删除数据
                if (ep) {
                    dellist.add(i);
                }
            }
            // 通过从数组后面开始删除,防止数据乱掉
            if (dellist.size() > 0) {
                for (int i = objArray.size() - 1; i >= 0; i--) {
                    if (dellist.contains(i)) {
                        objArray.remove(i);
                    }
                }
            }
        }
        // 如果为json对象
        else if (expect_json instanceof JSONObject) {
            JSONObject jsonObjectss = (JSONObject) expect_json;
            List<Object> itss = jsonObjectss.names();
            for (Object fieldName : itss) {
                if (excludes.length > 0 && Helper.isStringInArray(fieldName.toString(), excludes)) {
                    // 删除指定的key和key值
                    jsonObjectss.discard(fieldName.toString());
                    continue;
                }
                if (includes.length > 0 && !Helper.isStringInArray(fieldName.toString(), includes)) {
                    // 删除指定的key和key值
                    jsonObjectss.discard(fieldName.toString());
                    continue;
                }

                Object objectss = jsonObjectss.get(fieldName.toString());
                // 如果得到的是数组
                if (objectss instanceof JSONArray) {
                    JSONArray objArray = (JSONArray) objectss;
                    Helper.converterResponseBody(objArray, includes, excludes);
                }
                // 如果key中是一个json对象
                else if (objectss instanceof JSONObject) {
                    Helper.converterResponseBody(objectss, includes, excludes);
                }
            }

            // 如果是空对象就删除,不然如果是数组的话,就会重复返回空对象
            if (jsonObjectss.isNullObject() || jsonObjectss.isEmpty()) {
                isEmptyObject = true;
            }

        }
        return isEmptyObject;
    }

相信大家参考上面代码就能实现了,这里重点说2点:

  • 为了保持目前项目中上千个接口不影响,同时也不改变现有接口代码,于是额外新增2个参数:includes和excludes;

includes参数:返回前端个性化指定字段。

excludes参数:返回过滤指定字段后的的其它字段。

这2个参数通过拦截器,设置到HttpServletRequest中,然后我们就可以在beforeBodyWrite方法体中通过String includes_str = (String) request.getAttribute("includes")获取到。拦截器代码如下:

package com.example.demo.interceptor;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.Map;

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

import org.apache.commons.io.IOUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import com.example.demo.util.JsonUtils;

/**
 * @Type ModuleApiWebInterceptorHandler
 * @Desc MVC签名拦截器
 * @author 吴敏强
 * @date 2014-7-3
 * @Version V1.0
 */
public class ModuleApiWebInterceptorHandler extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        // Method:OPTIONS不验证
        String method = request.getMethod();
        if ("OPTIONS".equals(method)) {
            return true;
        }
        String url = request.getRequestURI();
        if (StringUtils.isEmpty(url)) {
            return true;
        }

        // 获取请求参数json串
        String info = "";
        if (!StringUtils.isEmpty(url)) {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            InputStream in = request.getInputStream();
            IOUtils.copy(in, os);
            // 重置流
            in.reset();
            // 获得源数据原始值,在签名校验处用到
            info = os.toString();
        }

        Map<String, Object> maps = JsonUtils.getMap4Json(info);
        String includes = null;
        String excludes = null;
        if (maps != null && !maps.isEmpty()) {
            includes = (String) maps.get("includes");
            excludes = (String) maps.get("excludes");
        }
        request.setAttribute("includes", includes);
        request.setAttribute("excludes", excludes);
        return super.preHandle(request, response, handler);
    }
}
  • 第二个重点要说的注意点是:对返回list数组类型需要做特殊处理。
 try {
            if (o instanceof List) {
                JSONArray json = JSONArray.fromObject(o);
                Helper.converterResponseBody(json, includes, excludes);
                return json;
            } else {
                JSONObject json = JSONObject.fromObject(o);
                Helper.converterResponseBody(json, includes, excludes);
                return json;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

其中对list转换需要使用JSONArray.fromObject(o)方法转换成JSONArray对象;其他类型使用JSONObject.fromObject(o)转换成JSONObject对象。经过上面处理我们就可实现动态返回json字段了。下面附上测试例子:

package com.example.demo;

import org.junit.Test;

import com.example.demo.controller.BaseParam;
import com.example.demo.controller.UserParam;
import com.example.demo.util.JsonUtils;

public class ControllerTest extends AbstractTest {

    @Test
    public void getbyid() throws Exception {
        UserParam param = new UserParam();
        param.setId(2);
        // param.setIncludes("id,name,list");
        param.setExcludes("name,password,list");
        super.httpPostWithJSON("/user/getbyid.json", JsonUtils.toJSON(param));
    }

    @Test
    public void all() throws Exception {
        BaseParam param = new BaseParam();
        param.setIncludes("id,name,adress");
        param.setExcludes("name,password");
        super.httpPostWithJSON("/user/all.json", JsonUtils.toJSON(param));
    }
}

跟原先测试用例相比就多了2个参数而已,其他都保持不变。到此基于ResponseBodyAdvice方式实现就讲完了,接下去说第2种实现方式。

3.2 基于MappingJackson2HttpMessageConverter方式实现

原先工程不是使用spring boot框架,而是spring mvc框架。在实际开发中安卓和ios前端2个小组,有时候存在对同一个接口双方处理方式也不一样的情况。比如返回对象类型空的话默认返回null,刚好安卓或ios没做null判断就发生闪退或其他异常情况,所以就做了一个对返回值是空情况特殊处理。

spring mvc默认是使用jackson序列化返回值对象的,对应的处理类是MappingJackson2HttpMessageConverter,此类继承了AbstractJackson2HttpMessageConverter抽象类,在这个抽象类中有一个ObjectMapper对象属性,这个对象就是最终序列化返回值的。ObjectMapper对象最终调用BeanPropertyWriter对象的serializeAsField方法,那么我们只要继承它重现它的serializeAsField就是实现了。

进一步查看代码,我们看到BeanPropertyWriter是通过BeanSerializerBuilder对象构造出来的,BeanSerializerBuilder自身又通过BeanSerializerFactory构造出来的,那么我们都自定义一个类,各自继承它们,重现它们对应方法即可。

首先我们看一下在工程的xml中如何配置指定自定义的类:

    <bean id="objectMapper" class="com.****.framework.moduleapi.converter.JSONObjectMapper"/>
    <bean id="jsonConverter" class="com.****.framework.moduleapi.converter.MobileMappingJacksonHttpMessageConverter">  
	  <property name="objectMapper" ref="objectMapper"/>  
    </bean> 
    <bean id="handlerAdapter"  class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">  
    <property name="messageConverters">  
       <list>
            <ref bean="jsonConverter" />  
       </list>  
    </property>
    <property name="order" value="0"/> 
    </bean> 

首先先自定义一个JSONObjectMapper对象:

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * @author wumq
 * 
 */
public class JSONObjectMapper extends ObjectMapper {
    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    public JSONObjectMapper() {
        super();
        // 设置输入时忽略在JSON字符串中存在但Java对象实际没有的属性
        disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        // 属性为 空(“”) 或者为 NULL 都不序列化
        // setSerializationInclusion(Include.NON_EMPTY);

        this._serializerFactory = new MobileBeanSerializerFactory(null);

    }

}

重点关注就是this._serializerFactory = new MobileBeanSerializerFactory(null);实例化我们自定义的factory。接下去我们继续自定义factory对象:

import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.cfg.SerializerFactoryConfig;
import com.fasterxml.jackson.databind.ser.BeanSerializerBuilder;
import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;

/**
 * @Type MobileBeanSerializerFactory
 * @Desc
 * @author 吴敏强
 * @date 2016年8月26日
 * @Version V1.0
 */
public class MobileBeanSerializerFactory extends BeanSerializerFactory {
    private static final long serialVersionUID = 4015397190053475450L;

    public MobileBeanSerializerFactory(SerializerFactoryConfig config) {
        super(config);
    }

    @Override
    protected BeanSerializerBuilder constructBeanSerializerBuilder(BeanDescription beanDesc) {
        return new MobileBeanSerializerBuilder(beanDesc);
    }
}

这里我们只需要再重写constructBeanSerializerBuilder方法,返回我们自定义的builder对象就可以了。

import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ser.BeanSerializer;
import com.fasterxml.jackson.databind.ser.BeanSerializerBuilder;

/**
 * @Type MobileBeanSerializerBuilder
 * @Desc
 * @author 吴敏强
 * @date 2016年8月26日
 * @Version V1.0
 */
public class MobileBeanSerializerBuilder extends BeanSerializerBuilder {

    private final static MobileBeanPropertyWriter[] NO_PROPERTIES = new MobileBeanPropertyWriter[0];

    public MobileBeanSerializerBuilder(BeanDescription beanDesc) {
        super(beanDesc);
    }

    protected MobileBeanSerializerBuilder(BeanSerializerBuilder src) {
        super(src);
    }

    @Override
    public JsonSerializer<?> build() {
        MobileBeanPropertyWriter[] properties;
        if (_properties == null || _properties.isEmpty()) {
            if (_anyGetter == null) {
                return null;
            }
            properties = NO_PROPERTIES;
        } else {
            properties = new MobileBeanPropertyWriter[_properties.size()];
            for (int i = 0; i < _properties.size(); i++) {
                properties[i] = new MobileBeanPropertyWriter(_properties.get(i));
            }
        }
        return new BeanSerializer(_beanDesc.getType(), this, properties, _filteredProperties);
    }
}

同样我们参考BeanSerializerBuilder父类代码,在build方法体中重点更改对属性操作的BeanPropertyWriter对象。

import java.util.List;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.io.SerializedString;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.util.Annotations;

/**
 * @Type MobileBeanPropertyWriter
 * @Desc
 * @author 吴敏强
 * @date 2016年8月26日
 * @Version V1.0
 */
public class MobileBeanPropertyWriter extends BeanPropertyWriter {
    private static final long serialVersionUID = 1L;

    protected MobileBeanPropertyWriter(BeanPropertyWriter base) {
        this(base, new SerializedString(base.getName()));
    }

    protected MobileBeanPropertyWriter(BeanPropertyWriter base, SerializedString name) {
        super(base, name);
    }

    public MobileBeanPropertyWriter(BeanPropertyDefinition propDef, AnnotatedMember member,
            Annotations contextAnnotations, JavaType declaredType, JsonSerializer<?> ser, TypeSerializer typeSer,
            JavaType serType, boolean suppressNulls, Object suppressableValue) {
        super(propDef, member, contextAnnotations, declaredType, ser, typeSer, serType, suppressNulls,
                suppressableValue);
    }

    @Override
    public void serializeAsField(Object bean, JsonGenerator gen, SerializerProvider prov) throws Exception {
        final Object value = (_accessorMethod == null) ? _field.get(bean) : _accessorMethod.invoke(bean);
        if (value == null) {
            if (_declaredType == null) {
                if (!_suppressNulls) {
                    gen.writeFieldName(_name);
                    prov.defaultSerializeNull(gen);
                }
            } else {
                gen.writeFieldName(_name);
                if (String.class.equals(_declaredType.getRawClass())) {
                    gen.writeString("");
                } else if (List.class.equals(_declaredType.getRawClass())) {
                    gen.writeStartArray();
                    gen.writeEndArray();
                } else if (Integer.class.equals(_declaredType.getRawClass())) {
                    gen.writeNumber(0);
                } else if (Long.class.equals(this._declaredType.getRawClass())) {
                    gen.writeNumber(0L);
                } else if (Float.class.equals(_declaredType.getRawClass())) {
                    gen.writeNumber(0.0F);
                } else if (Double.class.equals(_declaredType.getRawClass())) {
                    gen.writeNumber(0.0);
                } else {
                    // 安卓版本对{}返回还是代表存在对象,所以暂还是返回null处理
                    prov.defaultSerializeNull(gen);
                }
            }
            return;
        } else {
            super.serializeAsField(bean, gen, prov);
        }
    }
}

同样参考父类代码,重写serializeAsField方法就能最终达到变更返回值目的。比如String类型如果是null则返回“”、数值类型如果是null则返回0,详细直接参考上述源代码。到此基于jackson序列化原理也实现了。上面2种实现方式都是我实际项目中花了很多时间琢磨出来的,如果大家也在思索这方面需求,那么刚好也能用的上。

第一次写博客,语句组织的不是很好,希望大家指出。基于对返回值个性化的思路,实际项目中我们还可以对http请求参数做统一处理,比如通过自定义注解对参数格式或值做统一验证,对应RequestBodyAdvice接口和MethodInterceptor接口都能实现,如果大家感兴趣自己去研究一下即可。


评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值