Перейти к основному содержимому

Чтение HttpServletRequest несколько раз весной

· 4 мин. чтения

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() . Мы будем использовать этот метод для чтения необработанного тела и преобразования его в объект.

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

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

4.3. получитьчитатель()

Затем мы переопределим метод 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 () . В этом методе мы будем вызывать ByteArrayInputStream#read:

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

5.3. закончен()

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

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

5.4. готово()

Точно так же мы переопределим метод 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 .