SpringMVC返回结果“统一组装”介绍

来源:互联网 发布:js 将值放入option 编辑:程序博客网 时间:2024/06/06 02:23
                       SpringMVC返回结果“统一组装”介绍
  1. 问题背景:
    在web开发过程中,我们常常会提供两类接口。一类是内部接口,这类接口主要为web页面服务的,web前端可以通过ajax等手段进行后台数据的获取并渲染页面;另一类接口主要是供第三方使用,以便于与第三方系统集成。

但不管是哪一种接口,不管返回数据内容如何千变万化, 为了保证返回数据的可读性,通常都会采用统一的格式封装数据值。本文假设接口返回数据格式是json。比如,我们需要获取用户张三的个人信息,张三的个人信息以json格式表示如下形式所示:

{   "age": 28,   "depart": "默认控制中心",   "id": 1501743556176,   "name": "张三"}

但实际上接口返回的数据往往是下面这一种形式:

{   "data": {     "age": 28,     "depart": "默认控制中心",     "id": 1501743556176,     "name": "张三"  },   "errorCode": 0,   "message": "获取用户信息成功",   "success": true}

对比上述2个json数据体,我们可以看到,第二个json数据体格式更加复杂,该json数据体除了包含张三本人的身份信息以外,还额外包含了错误码(errorCode)、提示信息(message)、调用接口成功还是失败的标识(success)等字段。张三的个人信息只是这个更加复杂的json数据的data字段而已。

对比上述两个json数据体,我们可以知道上述接口将数据统一为如下形式之后才返回给调用者,这样一来,不管接口获取的是什么数据(文件流、字符串(非json形式的字符串)除外),序列化后的数据本身都仅仅是一个更大的json对象的data字段而已,与data字段处于同一级别的额外信息还包括errorCode、message、success等字段。

{   "data": Obj,   "errorCode": 0,   "message": "数据获取成功",   "success": true}

采用统一的数据格式封装数据的好处不言而喻。比如在ajax调用数据成功以后,ajax根据返回的数据的result字段是true还是false,分别弹出不同颜色的提示框,以便区分数据到底是获取成功还是获取失败。然后,这个弹出的提示框往往会有一条提示信息,以便让用户知道,这个接口是什么原因导致调用失败的,而用于渲染提示框的提示信息,一般就取自message字段。

  1. 问题的产生:
    在上一小节(问题背景)中,笔者介绍了统一接口数据格式这种业界通用的做法。然而,尽管大家都有一个共识:返回的数据形式应该统一一致,便于接口调用者获取数据后的解析(比如调用者可以根据具体格式封装一套通用的数据解析函数或工具),或者便于调用者从返回数据上可以获取更多与调用相关的上下文信息(比如,为什么调用失败,或者为什么调用成功等)。

然而,一致的共识只代表大家都采用了统一的规范,但这只是一个概念模型。具体大家是如何实现的,就因人而异了。

本人看到过有的项目中为了达到数据格式的统一,采用一个表示结果的对象(比如下文的ActionResult对象)封装返回的数据。然后,每一个方法的返回结果都是这个结果对象。每个方法在返回之前都会创建一个ActionResult对象实例,然后再调用这个对象的get或者set方法组装数据后返回。这种做法虽然是正确的,但是,在大型系统中,假设有10000个接口,这种做法直接导致相同的代码重复了10000遍。并且,这种统计方式的前提是我们仅仅假设所有返回数据都是在控制层(Action层或者Controller层)进行组装,但事实上,在业务层(service)的大多数方法的结果返回值也是ActionResult对象实例,这样一来,重复代码就更多了。

代码的重复导致的危害,一是降低了可读性,维护困难。二是降低了开发效率,虽然重复代码可以直接copy修改,但是也需要消耗开发时间。三,不优雅的代码直接降低了开发人员编码的热情,每天重复做事,暗无天日,对于有代码洁癖者,看到这样的代码更是一个莫大的打击。最重要的是,这些问题会逐渐磨损开发人员的热情,后续可能会放大这个问题(很多新员工的代码风格会在模仿过程中“走火入魔”)。

在这种实现接口格式统一的做法之下,我们会在子系统中看到无数类似下面的方法签名,这些方法返回结果都是ActionResult对象,本人将方法名写为method表示方法名是任意的,用于表明这种编码方式及其普遍,没有特指某个接口或某个方法。另外使用多个arg开头的参数表明这些方法的参数类型也是任意的,仅仅返回值都是ActionResult对象, 比如:

@RequestMapping(value= url,method= get|post|delete|put)@ResponseBodypublic ActionResult method(arg1, arg2, arg3 ……)

或者,我们还会看到另外一种形式的签名:

@RequestMapping(value= url,method= get|post|delete|put)@ResponseBodypublic void method(arg1, arg2, arg3 ……) {ActionResult actionResult =   write(JsonUtils.object2Json(actionResult));//将结果写入响应流}

注意,上述多次提到的ActionResult类代码如下所示:

public class ActionResult {    private boolean success;    private String message;    private Object data;private Integer code;    public ActionResult() {    }    public ActionResult(boolean success) {        this(success, null, null);    }    public ActionResult(boolean success, String message) {        this(success, message, null);    }    public ActionResult(boolean success, String message, Object data) {        this.success = success;        this.message = message;        this.data = data;    }      public boolean isSuccess() {        return success;    }     public void setSuccess(boolean success) {        this.success = success;    }     public String getMessage() {        return message;    }    public void setMessage(String message) {        this.message = message;    }     public Object getData() {        return data;    }       public void setData(Object data) {        this.data = data;    }}
  1. 使用springmvc实现结果统一装配:
    在之前的小节中, 本人阐述了项目中为了保证接口返回值数据格式的统一导致的重复代码问题。从spring mvc 4.0起引入RequestBodyAdvice和ResponseBodyAdvice这两个接口之后,之前描述的代码重复问题就可以彻底解决了。其中,RequestBodyAdvice主要用于对请求参数的增强处理。而ResponseBodyAdvice用于对响应参数的增强处理。所以,为了使返回的数据格式统一,很明显,我们只需要自己写一个实现ResponseBodyAdvice接口的类,并作为一个bean对象注入spring容器中即可。具体步骤一下:
    1 首先,需要写一个实现了ResponseBodyAdvice接口的类,此处我们将该类命名为MyResponseBodyAdvice。代码如下所示,注意:MyResponseBodyAdvice的supports方法实现表明,只有序列化框架是Jackson才对返回结果进行增强,具体项目中使用了哪一种序列化框架,修改该方法的实现即可。MyResponseBodyAdvice的beforeBodyWrite方法表明,Jackson序列化框架序列化的是增强的结果对象,而不是Actioc中接口方法返回值代表的那个对象,具体增强处理方式是:如果对象为空,不进行增强处理;如果结果返回对象是ActionResult对象或String对象,也不进行增强处理,直接返回。除上述两种情况以外,则新建一个ActionResult对象,然后把返回值作为这个新建的ActionResult对象的data字段,然后返回ActionResult对象,这样一来,框架的扩展功能就自动帮助我们进行了结果返回值的统一格式转换,从而实现,一处编写,“到处”可用。MyResponseBodyAdvice代码如下:
import java.io.File;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;import org.springframework.core.MethodParameter;import org.springframework.http.MediaType;import org.springframework.http.converter.HttpMessageConverter;import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;import org.springframework.http.server.ServerHttpRequest;import org.springframework.http.server.ServerHttpResponse;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;import com.core.constant.StatusCode;import com.core.springmvc.ActionResult;@ControllerAdvicepublic class MyResponseBodyAdvice implements ResponseBodyAdvice<Object> {    private static Log log = LogFactory.getLog(MyResponseBodyAdvice.class);    @Override    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {        log.debug("MyResponseBodyAdvice==>supports:" + converterType);        log.debug("MyResponseBodyAdvice==>supports:" + returnType.getClass());        log.debug("MyResponseBodyAdvice==>supports:"                + MappingJackson2HttpMessageConverter.class.isAssignableFrom(converterType));        return MappingJackson2HttpMessageConverter.class.isAssignableFrom(converterType);    }    @Override    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,                                  Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,                                  ServerHttpResponse response) {        if (body == null) {            return body;        }        if (body instanceof ActionResult || body instanceof String) {            return body;        } else if (body instanceof File) {            return body;        } else {            log.debug("MyResponseBodyAdvice==>beforeBodyWrite:" + returnType + "," + body);            ActionResult result = new ActionResult(true);            result.setCode(StatusCode.OK);            result.setData(body);            body = (Object) result;            // body.setB("我是后面设置的");            return body;        }    }}

2 将MyResponseBodyAdvice对象注入spring的IOC容器,比如可以spring配置文件中添加如下配置内容, 注意:这里,MyResponseBodyAdvice的包路径是com.core.converter:

<bean class="com.core.converter.MyResponseBodyAdvice"></bean>  

3 给自己实现的类MyResponseBodyAdvice添加注解@ControllerAdvice即可(上述类代码中已经添加)。

  1. 开启结果统一装配前后代码风格对比:
    1 对于有返回值的方法,比如get查询方法,使用结果统一装配之前代码是这样的:
@RequestMapping("/getUser")@ResponseBodypublic ActionResult getUser(Integer id) {   ActionResult result = new ActionResult(false);      User user = … //具体业务逻辑获取用户信息      result.setData(user);      result.setSuccess(true);      return result;}

使用结果统一装配之后的代码是这样的(通过方法的返回值就可以知道方法调用返回的结果是什么,再也不需要直接跟讨厌的ActionResult对象打交道了):

@RequestMapping("/getUser")@ResponseBodypublic User getUser(Integer id) {   User user = … //具体业务逻辑获取用户信息   return user;}

然而, 最终的返回结果都是一样的,比如

{   "data": {     "age": 28,     "depart": "默认控制中心",     "id": 1501743556176, //用户id    "name": "张三" //用户名字  },   "errorCode": 0,   "message": "",   "success": true}
  1. 使用结果统一装配后如何实现业务信息提示:

如上述代码所示,使用springmvc的结果统一装配功能后,对于有返回值的方法,需要返回什么类型的数据,则直接返回指定类型的对象实例即可。逻辑清晰明了,不需要每一个方法都返回千篇一律的ActionResult对象,这样一来,冗余代码可以消除。

但同时,这种做法产生了另外一个问题。事实上,ActionResult除了可以装载返回结果值以外。另一个重要的作用就是装载提示信息,比如参数校验不通过返回的提示信息是通过给ActionResult的message字段赋值然后返回。现在使用了结果统一装配之后,方法提示信息无法携带。为了解决这个问题,可以使用spring mvc等框架提供的统一异常处理机制解决,对于参数校验不通过的情况,直接抛出自定义的参数异常或业务异常(因为通常不应该直接暴露底层的异常信息给调用方,暴露的异常必须要转换为上层的业务类异常再抛出),鉴于抛出异常具有中断方法执行的效果,与return关键具有中断代码执行的的作用。在代码中使用结果统一装配和统一异常处理可以称作是比较“般配”或“绝配”的组合方式,可以消除许多中情况下的if else的嵌套代码。所以这种组合使用方式值得推荐。限于篇幅,关于统一异常处理的问题,可以参见本人的另外一篇博文,不在赘述!

  1. spring mvc 4.0之前版本如何实现结果统一装配
    由于结果统一装配功能是spring mvc 4.0之后的版本,所以在4.0以前版本中,要实现相同的目的,就只能使用sping的AOP功能了。通常做法如下:

比如使用注解要在Spring配置文件中添加

在自定义的Aspect类上添加注解@Aspect以及SpringIOC的@Component

首先注册切入点
@Pointcut(“execution(* com.liyunpeng.www.gateway.controller..(..))”)
public void resultMapAspect(){}

再根据切入点,配置相应的执行方法
@AfterReturning(value = “resultMapAspect()”,returning=”resultMap”)
public void abc(JoinPoint joinpoint,Object resultMap) throws Throwable {
此处的resultMap就是通过Controller之后的返回值了,我们可以进行处理相关的内容

另外,也可以通过aop:config标签来配置aop
除了afterReturning意外,还有before around after,分别是执行前、执行前+后、执行后
只不过afterReturning可以获取对应的返回值和参数。
6.总结
本文主要介绍了如何利用Spring MVC的结果统一装配方式(在struts2和目前流行的jersey框架中是从诞生起就支持的,然而,在spring mvc中直到4.0之后才支持)消除实际项目重复率较高的冗余代码,从而保持代码的精简和优雅,易读和易维护。

当然了,正如文中所述,spring mvc的结果统一装配方式结合统一异常处理一起使用可以让代码更加完美,关于统一异常处理的问题,可以参见本人的另外一篇经验案例: 《关于合理使用SpringMVC统一异常处理机制以改善代码风格的一些思考.docx》

由于spring mvc的结果统一装配功能是在4.0之后才开始支持的,但4.0之前的版本也可以通过AOP的方式实现,本文也有所阐述。

阅读全文
0 0