用一个切面来统一返回前端的JSON格式

新项目使用Spring MVC + MyBatis架构来做,这套框架自己应该比较得心应手,这里来写一下这两天做的一些设计。

首先是静态资源的处理,关于这个之前有写文章单独讲过,这里不再重复写,不太清楚的童鞋可以移步查看

接着需要统一JSON的返回格式,和前端工程师约定,对于字符串类型和日期类型都返回字符串,而对于普通数字类型的话都返回数字,金额类数字都返回格式化好的保留一位小数的字符串(比如”10.0”),另外数字和金额的默认值为0,而字符串的默认值为字符串空(“”),并且对于返回的格式也有规范,所有的JSON返回必须形如:

{
    "status": 1,
    "data": data,
    "msg": ""
}

其中:

  • status代表了该请求的成功与否,若为1表示成功,若为0表示失败,此时msg中将包含显示给用户的错误信息
  • data代表了返回给前端的数据,可以是一个复杂的数据结构
  • msg前面提到了,一般放异常信息。

下面,我们首先需要对默认的Jackson的序列化规则做出修改,这里自定义一个ObjectMapper

/**
 * 自定义jackson解析器
 * @author Zhu
 * @date 2015-5-14
 * @version 0.0.1
 * @description
 */
@Component
public class CustomerObjectMapper extends ObjectMapper {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    /**
     * 
     */
    public CustomerObjectMapper() {

        super();
//      this.setSerializationInclusion(Include.NON_NULL);
        // 允许单引号
//        this.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
        // 字段和值都加引号
//        this.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
        // 数字也加引号
//        this.configure(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS, true);
//        this.configure(JsonGenerator.Feature.QUOTE_NON_NUMERIC_NUMBERS, true);
        this.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>(){
            @Override
            public void serialize(Object value, JsonGenerator generator,
                    SerializerProvider provider) throws IOException,
                    JsonProcessingException {
                generator.writeString("");
            }
        });
    }

}

然后将我们自定义的ObjectMapper注入到MappingJackson2HttpMessageConverter中去并重新配置Spring MVC中的message converter

    <bean id="mappingJackson2HttpMessageConverter"
        class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
        <property name="objectMapper" ref="customerObjectMapper"></property>
    </bean>

    <mvc:annotation-driven>
        <mvc:message-converters>
            <ref bean="mappingJackson2HttpMessageConverter"/>
        </mvc:message-converters>
    </mvc:annotation-driven>

接下来,我们可以在Controller方法上添加@ResponseBody注解来返回JSON(这里需要说一句,classpath下需要有jackson的jar包,这是前提),但是这就要求我们在每个Controller方法中都进行一些重复的工作,比如:

    public ModelMap getUserList(){
        ModelMap modelMap = new ModelMap();
        try {
            modelMap.put("status", 1);
            modelMap.put("data", userSerivce.getUserList());
            modelMap.put("", "");
        } catch (Exception e) {
            modelMap.put("status", 0);
            modelMap.put("data", "");
            modelMap.put("msg", e.getMessage());
            logger.error("getUserList occurs error:", e);
        }
    }

这样的重复代码在项目中不建议出现,我们其实只需要关心其中的data就可以了,所以我打算将其统一写到一个方法中,并且这也有利于以后对于公共结构的更改。下面就是一个切面来统一处理所有的接口:

/**
 * 其中ResponseBase类和注解类CustomResponseBody都是自己写的
 */

/**
 * @author Zhu
 * @date 2015-5-18
 * @version 0.0.1
 * @description 负责将返回转换成统一消息格式
 * 序列化为的格式如下:
 * {
    "status": 1,
    "data": data,
    "msg": ""
    }
    其中被注解的方法只需要关心data的内容即可
 */
@Aspect
@Component
public class ResponseAspect {

    @Resource
    private MappingJackson2HttpMessageConverter converter;

    private final Logger logger = LoggerFactory.getLogger(getClass());


    /**
     * 拦截所有@ResponseBody
     * @author Zhu
     * @date 2015-5-18上午11:32:26
     * @description
     */
    @Pointcut("execution(* com.xxx.*.web.controller.*.*(..)) && @annotation(com.xxx.common.annotation.CustomResponseBody)")
    public void responseBodyPointCut() {

    }

    /**
     * @author Zhu
     * @date 2015-5-18上午11:35:42
     * @description 
     * @param pjp
     * @throws Throwable 
     */
    @Around(value = "responseBodyPointCut()")
    @ResponseBody
    public void formatResult2JSON(ProceedingJoinPoint pjp) throws Throwable {
        Object ret = pjp.proceed();
        ResponseBase responseBase = new ResponseBase();
        responseBase.setData(ret);

        HttpServletResponse response = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getResponse();
        HttpOutputMessage outputMessage = new ServletServerHttpResponse(response);
        converter.write(responseBase, MediaType.APPLICATION_JSON, outputMessage);
        shutdownResponse(response);
    }

    /**
     * 
     * @author Zhu
     * @date 2015-5-18下午6:01:46
     * @description 
     * @param jp
     * @param error
     * @throws Throwable
     */
    @AfterThrowing(pointcut = "responseBodyPointCut()", throwing = "error")
    public void handleForException(JoinPoint jp, Throwable error) throws Throwable{
        ResponseBase responseBase = new ResponseBase();
        responseBase.setStatus(0);
        responseBase.setMsg(error.getMessage());
        HttpServletResponse response = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getResponse();
        logger.error(jp.getSignature().getName() + "-error!", error);
        HttpOutputMessage outputMessage = new ServletServerHttpResponse(response);
        converter.write(responseBase, MediaType.APPLICATION_JSON, outputMessage);
        shutdownResponse(response);
    }

    private void shutdownResponse(HttpServletResponse response) throws IOException{
        response.getOutputStream().close();
    }
}

当然这里有几个关键点,需要提一下:

  1. 这也就提到了一个如何在Spring中手动使用配置好的Jackson来序列化输出,这里我注入MappingJackson2HttpMessageConverter,因为这里想要统一项目的JSON序列化都交由Jackson来做
  2. 由于是对Controller的代理,而一般Controller是不实现接口的,那么就无法使用JDK自带的动态代理,需要用到cglib来做
  3. 关于上下文的问题,这里我在做的时候,由于碰到了两个上下文,即一般所说的rootContext和webContext。。。。
  4. 在自己返回json的情况下,需要shutdownResponse,即将Response关掉,不然可能会在这里输出json后还会再次输出一些内容

其余的内容后续再写吧,现在手头上项目太多,需要整理的东西也太多了~

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。