Spring MVC: как изменить ответ json, отправленный с контроллера

Я построил службу REST json с такими контроллерами, как этот:

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

    @Autowired
    private ScriptService scriptService;

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

Он отлично работает, но теперь мне нужно изменить все ответы и добавить все поля "статус" и "сообщение". Я читал о некоторых решениях:

  • возвращает из всех методов контроллера объект некоторого определенного класса, например, RestResponse, который будет содержать поля "статус" и "сообщение" (но это не общее решение, потому что мне придется модифицировать все мои контроллеры и писать новые контроллеры в новом стиле)
  • перехватить все методы контроллера с помощью аспектов (но в этом случае я не могу изменить тип возврата)

Можете ли вы предложить другое, общее и правильное решение, если я хочу обернуть значения, возвращаемые из методов контроллера, в объекты класса:

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

Я столкнулся с подобной проблемой и предлагаю вам использовать фильтры сервлетов, чтобы решить проблему.

Сервлет-фильтры - это Java-классы, которые могут использоваться в сервлет-программировании для перехвата запросов от клиента до того, как они обратятся к ресурсу в конце или для управления ответами от сервера, прежде чем они будут отправлены обратно клиенту.

Ваш фильтр должен реализовать интерфейс javax.servlet.Filter и переопределить три метода:

public void doFilter (ServletRequest, ServletResponse, FilterChain)

Этот метод вызывается каждый раз, когда пара запроса/ответа передается по цепочке из-за запроса клиента для ресурса в конце цепочки.

public void init(FilterConfig filterConfig)

Вызывается перед включением фильтра и устанавливает объект конфигурации фильтра.

public void destroy()

Вызывается после того, как фильтр был выведен из эксплуатации.

Существует возможность использовать любое количество фильтров, а порядок выполнения будет таким же, как и порядок, в котором они определены в web.xml.

web.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, добавляет как feild к вашему классу класса RestResponse (со статусом и полями сообщений), сериализует его объект в Json и отправляет полный ответ клиенту.

Класс ResponseFilter:

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) вызывает следующий фильтр в цепочке или если вызывающий фильтр является последним фильтром в цепочке, вызывает логику сервлетов.

Обертка ответа сервлетов HTTP использует настраиваемый выходной поток сервлета, который позволяет оболочке манипулировать данными ответа после того, как сервлет завершил запись его. Обычно это невозможно сделать после того, как выходной поток сервлета закрыт (по сути, после того, как сервлет совершил это). Вот почему для реализации расширения ServletOutputStream используется расширение, специфичное для фильтра.

ФильтрServletOutputStream класс:

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 должен быть реализован класс, который может действовать как объект ответа. Этот объект-оболочка отправляется обратно клиенту вместо исходного ответа, сгенерированного сервлетом.

Класс 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 для настройки ответа перед написанием тела.