1. 소개

이 예제에서는  Spring을 사용하여 HttpServletRequest 에서 본문을 여러 번 읽는 방법을 배웁니다 .

HttpServletRequest  본문을 읽기 위해 getInputStream () 메소드를 노출하는 인터페이스입니다 . 기본적 으로이 InputStream 의 데이터는 한 번만 읽을 수 있습니다 .

2. Maven 의존성

가장 먼저 필요한 것은 적절한 spring-webmvcjavax.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 확장

HttpServletRequestWrapper 를 확장 하는 새로운 클래스 CachedBodyHttpServletRequest를 만들어 보겠습니다 . 이렇게하면 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 구현

ServletInputStream 을 구현할 클래스 CachedBodyServletInputStream을 만들어 보겠습니다 . 이 클래스에서는 새 생성자를 만들고 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 에 읽을 데이터가 더 있는지 여부를 나타냅니다 . 읽을 수있는 바이트가 0 인 경우 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 클래스 의 새로운 구현을 만들었습니다 . ServletInputStream 클래스 의 객체를 반환 하도록 getInputStream () 메서드를 재정의했습니다  .

마지막으로 요청 래퍼 개체를 필터 체인에 전달하는 새 필터를 만들었습니다. 그래서 우리는 요청을 여러 번 읽을 수있었습니다.

예제의 전체 소스 코드는 GitHub 에서 찾을 수 있습니다  .