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 .