一:应用背景
在介绍功能之前,先说一下工作中遇到的问题。项目中服务端提供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接口都能实现,如果大家感兴趣自己去研究一下即可。