如何在Spring中包装JSON响应

时间:2014-06-06 00:38:45

标签: json spring spring-mvc

假设我在Spring中有两组控制器:

  • /jsonapi1/*
  • /jsonapi2/*

这两个对象都返回要解释为JSON文本的对象。

我希望使用某种过滤器来包装来自这些控制器的响应,以便:

  1. 原始回复包含在另一个对象中。

    例如,如果/ jsonapi1 / count返回:

    {"num_humans":123, "num_androids":456}
    

    然后应该包装并返回响应,如下所示:

    { "status":0,
      "content":{"num_humans":123, "num_androids":456}
    }
    
  2. 如果控制器中发生异常,则过滤器应捕获异常并按如下方式报告

    { "status":5,
      "content":"Something terrible happened"
    }
    
  3. 其他控制器的响应将保持不变。

  4. 我们目前正在自定义传递给MappingJackson2HttpMessageConverter的{​​{1}}以执行上述任务。效果很好,但这种方法似乎不可能选择适用于它的URL(或控制器类)。

    是否可以将这些类型的包装器应用于单个控制器类或URL?


    更新:Servlet过滤器看起来像一个解决方案。是否可以选择将哪个过滤器应用于哪些控制器方法或哪些URL?

4 个答案:

答案 0 :(得分:7)

我理解你的问题的方式,你有三个选择。

选项#1

手动将对象包装在简单的SuccessResponseErrorResponseSomethingSortOfWrongResponse等具有所需字段的对象中。此时,您具有每个请求的灵活性,更改其中一个响应包装器上的字段是微不足道的,唯一真正的缺点是代码重复,如果许多控制器的请求方法可以并且应该组合在一起。

选项#2

正如您所提到的,过滤器可以设计用于执行脏工作,但要小心Spring过滤器不会授予您访问请求或响应数据的权限。以下是它的外观示例:

@Component
public class ResponseWrappingFilter extends GenericFilterBean {

    @Override
    public void doFilter(
        ServletRequest request,
        ServletResponse response,
        FilterChain chain) {

        // Perform the rest of the chain, populating the response.
        chain.doFilter(request, response);

        // No way to read the body from the response here. getBody() doesn't exist.
        response.setBody(new ResponseWrapper(response.getStatus(), response.getBody());
    }
} 

如果您找到了在该过滤器中设置主体的方法,那么是的,您可以轻松地将其包裹起来。否则,这个选项就是死路一条。

选项#3

A-公顷。所以你到目前为止。代码重复不是选项,但坚持包装来自控制器方法的响应。我想介绍真正的解决方案 - 面向方面的编程(AOP),Spring非常支持。

如果您不熟悉AOP,前提如下:您在代码中定义匹配(如正则表达式匹配)点的表达式。这些点称为连接点,而与它们匹配的表达式称为 pointcuts 。然后,当匹配任何切入点或切入点组合时,您可以选择执行名为 advice 的其他任意代码。定义切入点和建议的对象称为 aspect

在Java中表达自己更流利是很棒的。唯一的缺点是较弱的静态类型检查。不用多说,这是你在面向方面编程中的回应:

@Aspect
@Component
public class ResponseWrappingAspect {

    @Pointcut("within(@org.springframework.stereotype.Controller *)")
    public void anyControllerPointcut() {}

    @Pointcut("execution(* *(..))")
    public void anyMethodPointcut() {}

    @AfterReturning(
        value = "anyControllerPointcut() && anyMethodPointcut()",
        returning = "response")
    public Object wrapResponse(Object response) {

        // Do whatever logic needs to be done to wrap it correctly.
        return new ResponseWrapper(response);
    }

    @AfterThrowing(
        value = "anyControllerPointcut() && anyMethodPointcut()",
        throwing = "cause")
    public Object wrapException(Exception cause) {

        // Do whatever logic needs to be done to wrap it correctly.
        return new ErrorResponseWrapper(cause);
    }
}

最终结果将是您寻求的非重复响应包装。如果您只希望某个或一个控制器收到此效果,则更新切入点以仅匹配该控制器实例内的方法(而不是任何持有@Controller注释的类)。

您需要包含一些AOP依赖项,在配置类中添加启用AOP的注释,并确保组件扫描此类所在的包。

答案 1 :(得分:3)

我为此苦苦挣扎了好几天。 @Misha的解决方案对我不起作用。我终于能够使用ControllerAdviceResponseBodyAdvice来完成这项工作。

ResponseBodyAdvice允许对控制器返回的响应注入自定义转换逻辑,但该响应必须转换为HttpResponse并提交。

这是我的控制器方法的外观:

@RequestMapping("/global/hallOfFame")
    public List<HallOfFame> getAllHallOfFame() {
        return hallOfFameService.getAllHallOfFame();
}

现在,我想在响应周围添加一些标准字段,例如devmessageusermessage。该逻辑进入ResponseAdvice:

@ControllerAdvice
public class TLResponseAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
            Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
        ServerHttpResponse response) {
        // TODO Auto-generated method stub
        final RestResponse<Object> output = new RestResponse<>();
        output.setData(body);
        output.setDevMessage("ResponseAdviceDevMessage");
        output.setHttpcode(200);
        output.setStatus("Success");
        output.setUserMessage("ResponseAdviceUserMessage");
        return output;
    }
}

实体类如下:

@Setter // All lombok annotations
@Getter
@ToString
public class RestResponse<T> {

    private String status;
    private int httpcode;
    private String devMessage;
    private String userMessage;

    private T data;
}

@Entity
@Data // Lombok
public class HallOfFame {

    @Id
    private String id;
    private String name;
}

要处理异常,只需使用ControllerAdvice创建另一个ExceptionHandler。使用示例in this link

此解决方案的优点:

  1. 它可以使您的控制器保持清洁。您可以从控制器方法中支持任何返回类型。
  2. 您的控制器返回类型类不需要按照AOP方法的要求扩展某些基类。
  3. 您不需要使用HttpServletResponseWrappers来遍历Spring过滤器。他们提出了性能惩罚。

答案 2 :(得分:1)

我从控制器管理自定义响应的最简单方法是使用Map变量。

所以你的代码最终看起来像:

public @ResponseBody Map controllerName(...) {

Map mapA = new HashMap();

mapA.put("status", "5");
mapA.put("content", "something went south");

return mapA;

}

美丽的是,你可以配置它任何一种方式。 目前我用于对象传输,自定义异常处理和数据报告,太容易了。

希望这有帮助

答案 3 :(得分:0)

我也在使用AOP和@Around。开发了自定义注释并将其用于切入点。我正在使用全球响应。它具有状态,消息和数据类型为List的类型

List <? extends parent> dataList

(可以解决你的类强制转换异常)。所有实体都扩展了此Parent类。这样我就可以将所有数据都设置到我的列表中。 此外,我使用消息键作为自定义注释的参数并将其设置为运行。  希望这会有所帮助。