Spring MVC:如何修改从控制器发送的json响应

2022-09-01 15:18:01

我已经用这样的控制器构建了一个json REST服务:

@Controller
@RequestMapping(value = "/scripts")
public class ScriptController {

    @Autowired
    private ScriptService scriptService;

    @RequestMapping(method = RequestMethod.GET)
    @ResponseBody
    public List<Script> get() {
        return scriptService.getScripts();
    }
}

它工作正常,但现在我需要修改所有响应并向所有响应添加“状态”和“消息”字段。我已经阅读了一些解决方案:

  1. 从某些特定类的所有控制器方法对象返回,例如,RestResponse,它将包含“状态”和“消息”字段(但这不是通用解决方案,因为我将不得不修改所有控制器并以新样式编写新控制器)
  2. 拦截所有具有方面的控制器方法(但在这种情况下,我无法更改返回类型)

如果我想将从控制器方法返回的值包装到类的对象中,你能建议一些其他的,通用的和正确的解决方案吗:

public class RestResponse {

    private int status;
    private String message;
    private Object data;

    public RestResponse(int status, String message, Object data) {
        this.status = status;
        this.message = message;
        this.data = data;
    }

    //getters and setters
}

答案 1

我遇到了类似的问题,并建议您使用Servlet过滤器来解决这个问题。

Servlet 过滤器是 Java 类,可以在 Servlet 编程中使用,在客户端访问后端资源之前拦截来自客户端的请求,或者在将来自服务器的响应发送回客户端之前操作它们。

您的过滤器必须实现javax.servlet.Filter接口并覆盖三种方法:

public void doFilter (ServletRequest, ServletResponse, FilterChain)

每次请求/响应对通过链传递时,由于客户端对链末端的资源的请求,都会调用此方法。

public void init(FilterConfig filterConfig)

在筛选器投入使用之前调用,并设置筛选器的配置对象。

public void destroy()

在筛选器停止服务后调用。

可以使用任意数量的筛选器,并且执行顺序将与它们在 Web .xml 中定义的顺序相同。

网页.xml:

...
<filter>
    <filter-name>restResponseFilter</filter-name>
    <filter-class>
        com.package.filters.ResponseFilter
    </filter-class>
</filter>

<filter>
    <filter-name>anotherFilter</filter-name>
    <filter-class>
        com.package.filters.AnotherFilter
    </filter-class>
</filter>
...

因此,此筛选器获取控制器响应,将其转换为 String,作为字段添加到 RestResponse 类对象(具有状态和消息字段),将其对象序列化为 Json,并将完整的响应发送到客户端。

响应筛选器类:

public final class ResponseFilter implements Filter {

@Override
    public void init(FilterConfig filterConfig) {
}

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

    ResponseWrapper responseWrapper = new ResponseWrapper((HttpServletResponse) response);

    chain.doFilter(request, responseWrapper);

    String responseContent = new String(responseWrapper.getDataStream());

    RestResponse fullResponse = new RestResponse(/*status*/, /*message*/,responseContent);

    byte[] responseToSend = restResponseBytes(fullResponse);

    response.getOutputStream().write(responseToSend);

}

@Override
public void destroy() {
}

private byte[] restResponseBytes(RestResponse response) throws IOException {
    String serialized = new ObjectMapper().writeValueAsString(response);
    return serialized.getBytes();
}
}

chain.doFilter(request, responseWrapper) 方法调用链中的下一个过滤器,或者如果调用过滤器是链中的最后一个过滤器,则调用 servlet 逻辑。

HTTP servlet 响应包装器使用自定义 servlet 输出流,该流允许包装器在 servlet 写完整后操作响应数据。通常,这不能在 servlet 输出流关闭之后完成(实质上是在 servlet 提交它之后)。这就是向 ServletOutputStream 类实现特定于筛选器的扩展的原因。

FilterServletOutputStream 类:

public class FilterServletOutputStream extends ServletOutputStream {

DataOutputStream output;
public FilterServletOutputStream(OutputStream output) {
    this.output = new DataOutputStream(output);
}

@Override
public void write(int arg0) throws IOException {
    output.write(arg0);
}

@Override
public void write(byte[] arg0, int arg1, int arg2) throws IOException {
    output.write(arg0, arg1, arg2);
}

@Override
public void write(byte[] arg0) throws IOException {
    output.write(arg0);
}
}

若要使用 FilterServletOutputStream 类,应实现一个可用作响应对象的类。此包装器对象被发送回客户端,以代替 servlet 生成的原始响应。

ResponseWrapper 类:

public class ResponseWrapper extends HttpServletResponseWrapper {

ByteArrayOutputStream output;
FilterServletOutputStream filterOutput;
HttpResponseStatus status = HttpResponseStatus.OK;

public ResponseWrapper(HttpServletResponse response) {
    super(response);
    output = new ByteArrayOutputStream();
}

@Override
public ServletOutputStream getOutputStream() throws IOException {
    if (filterOutput == null) {
        filterOutput = new FilterServletOutputStream(output);
    }
    return filterOutput;
}

public byte[] getDataStream() {
    return output.toByteArray();
}
}

我认为这种方法将是您问题的良好解决方案。

请问一个问题,如果有什么不清楚的地方,如果我错了,请纠正我。


答案 2

如果您使用 spring 4.1 或更高版本,则可以在写入正文之前使用 ResponseBodyAdvice 自定义响应。