«1. Введение

В этом руководстве мы узнаем, как несколько раз читать тело из HttpServletRequest с помощью Spring.

HttpServletRequest — это интерфейс, который предоставляет метод getInputStream() для чтения тела. По умолчанию данные из этого InputStream можно прочитать только один раз.

2. Зависимости Maven

Первое, что нам нужно, это соответствующие зависимости spring-webmvc и javax.servlet:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.2.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
</dependency>

Кроме того, поскольку мы используем тип содержимого application/json, требуется зависимость jackson-databind:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.10.0</version>
</dependency>

Spring использует эту библиотеку для преобразования в JSON и обратно.

3. Spring ContentCachingRequestWrapper

Spring предоставляет класс ContentCachingRequestWrapper. Этот класс предоставляет метод getContentAsByteArray() для многократного чтения тела.

Этот класс имеет ограничение: мы не можем читать тело несколько раз, используя методы getInputStream() и getReader().

Этот класс кэширует тело запроса, используя InputStream. Если мы прочитаем InputStream в одном из фильтров, то другие последующие фильтры в цепочке фильтров больше не смогут его прочитать. Из-за этого ограничения этот класс подходит не во всех ситуациях.

Чтобы обойти это ограничение, давайте рассмотрим более универсальное решение.

4. Расширение HttpServletRequest

Давайте создадим новый класс — CachedBodyHttpServletRequest — который расширяет HttpServletRequestWrapper. Таким образом, нам не нужно переопределять все абстрактные методы интерфейса HttpServletRequest.

Класс HttpServletRequestWrapper имеет два абстрактных метода getInputStream() и getReader(). Мы переопределим оба этих метода и создадим новый конструктор.

4.1. Конструктор

Сначала создадим конструктор. Внутри него мы прочитаем тело из фактического InputStream и сохраним его в объекте byte[]:

public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {

    private byte[] cachedBody;

    public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        InputStream requestInputStream = request.getInputStream();
        this.cachedBody = StreamUtils.copyToByteArray(requestInputStream);
    }
}

В результате мы сможем читать тело несколько раз.

4.2. getInputStream()

Далее, давайте переопределим метод getInputStream(). Мы будем использовать этот метод для чтения необработанного тела и преобразования его в объект.

В этом методе мы создадим и вернем новый объект класса CachedBodyServletInputStream (реализация ServletInputStream):

@Override
public ServletInputStream getInputStream() throws IOException {
    return new CachedBodyServletInputStream(this.cachedBody);
}

4.3. getReader()

Затем мы переопределим метод getReader(). Этот метод возвращает объект BufferedReader:

@Override
public BufferedReader getReader() throws IOException {
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
    return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}

5. Реализация ServletInputStream

Давайте создадим класс — CachedBodyServletInputStream — который будет реализовывать ServletInputStream. В этом классе мы создадим новый конструктор, а также переопределим методы isFinished(), isReady() и read().

5.1. Конструктор

Во-первых, давайте создадим новый конструктор, который принимает массив байтов.

Внутри него мы создадим новый экземпляр ByteArrayInputStream, используя этот массив байтов. После этого присвоим его глобальной переменной cachedBodyInputStream:

public class CachedBodyServletInputStream extends ServletInputStream {

    private InputStream cachedBodyInputStream;

    public CachedBodyServletInputStream(byte[] cachedBody) {
        this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
    }
}

5.2. read()

Затем мы переопределим метод read(). В этом методе мы будем вызывать ByteArrayInputStream#read:

@Override
public int read() throws IOException {
    return cachedBodyInputStream.read();
}

5.3. isFinished()

Затем мы переопределим метод isFinished(). Этот метод указывает, есть ли у InputStream больше данных для чтения или нет. Возвращает true, когда доступно для чтения нулевое количество байтов:

@Override
public boolean isFinished() {
    return cachedBody.available() == 0;
}

5.4. isReady()

Точно так же мы переопределим метод isReady(). Этот метод указывает, готов ли InputStream к чтению или нет.

Поскольку мы уже скопировали InputStream в массив байтов, мы вернем true, чтобы указать, что он всегда доступен:

@Override
public boolean isReady() {
    return true;
}

6. Фильтр

Наконец, давайте создадим новый фильтр для использования класса CachedBodyHttpServletRequest. Здесь мы расширим класс Spring OncePerRequestFilter. Этот класс имеет абстрактный метод doFilterInternal().

В этом методе мы создадим объект класса CachedBodyHttpServletRequest из фактического объекта запроса:

CachedBodyHttpServletRequest cachedBodyHttpServletRequest =
  new CachedBodyHttpServletRequest(request);

Затем мы передадим этот новый объект-оболочку запроса в цепочку фильтров. Таким образом, все последующие вызовы метода getInputStream() будут вызывать переопределенный метод:

filterChain.doFilter(cachedContentHttpServletRequest, response);

7. Заключение

«В этом руководстве мы быстро прошлись по классу ContentCachingRequestWrapper. Мы также видели его ограничения.

Затем мы создали новую реализацию класса HttpServletRequestWrapper. Мы переопределили метод getInputStream(), чтобы он возвращал объект класса ServletInputStream.

Наконец, мы создали новый фильтр для передачи объекта-оболочки запроса в цепочку фильтров. Таким образом, мы смогли прочитать запрос несколько раз.

Полный исходный код примеров можно найти на GitHub.