คำขอ HTTP Servlet จะสูญเสียพารามิเตอร์จากเนื้อหา POST หลังจากอ่านครั้งเดียว


88

ฉันพยายามเข้าถึงพารามิเตอร์คำขอ http สองรายการในตัวกรอง Java Servlet ไม่มีอะไรใหม่ที่นี่ แต่รู้สึกประหลาดใจที่พบว่าพารามิเตอร์ถูกใช้ไปแล้ว! ด้วยเหตุนี้จึงไม่สามารถใช้ได้อีกต่อไปในห่วงโซ่ตัวกรอง

ดูเหมือนว่าสิ่งนี้จะเกิดขึ้นเฉพาะเมื่อพารามิเตอร์มาในเนื้อหาคำขอ POST (เช่นการส่งแบบฟอร์ม)

มีวิธีอ่านพารามิเตอร์และไม่ใช้มันหรือไม่?

เพื่อให้ห่างไกลที่ฉันได้พบเพียงเอกสารอ้างอิงนี้: Servlet กรองโดยใช้ request.getParameter สูญเสียข้อมูลในแบบฟอร์ม

ขอบคุณ!


2
อาจจะแสดงส่วนของโค้ดว่าคุณกำลังทำมันอย่างไร?
Pavel Veller

คุณได้รับ getInputStream () หรือ getReader () หรือไม่? ดูเหมือนว่าพวกเขาเป็นคนที่จะรบกวนการทำงานของ getParameter ()
evanwong

คำตอบ:


115

อีกทางเลือกหนึ่งในการแก้ปัญหานี้คืออย่าใช้ห่วงโซ่ตัวกรองและสร้างส่วนประกอบตัวสกัดกั้นของคุณเองแทนโดยอาจใช้แง่มุมซึ่งสามารถดำเนินการกับเนื้อหาคำขอที่แยกวิเคราะห์ได้ นอกจากนี้ยังมีแนวโน้มที่จะมีประสิทธิภาพมากขึ้นเนื่องจากคุณแปลงคำขอInputStreamเป็นโมเดลออบเจ็กต์ของคุณเองเพียงครั้งเดียว

อย่างไรก็ตามฉันยังคิดว่ามันสมเหตุสมผลที่จะต้องการอ่านเนื้อหาของคำขอมากกว่าหนึ่งครั้งโดยเฉพาะอย่างยิ่งเมื่อคำขอเคลื่อนผ่านห่วงโซ่ตัวกรอง โดยทั่วไปฉันจะใช้ฟิลเตอร์เชนสำหรับการดำเนินการบางอย่างที่ฉันต้องการเก็บไว้ที่เลเยอร์ HTTP โดยแยกออกจากคอมโพเนนต์บริการ

ตามที่แนะนำโดยWill Hartungฉันทำได้โดยการขยายHttpServletRequestWrapperใช้งานคำขอInputStreamและแคชไบต์เป็นหลัก

public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
  private ByteArrayOutputStream cachedBytes;

  public MultiReadHttpServletRequest(HttpServletRequest request) {
    super(request);
  }

  @Override
  public ServletInputStream getInputStream() throws IOException {
    if (cachedBytes == null)
      cacheInputStream();

      return new CachedServletInputStream();
  }

  @Override
  public BufferedReader getReader() throws IOException{
    return new BufferedReader(new InputStreamReader(getInputStream()));
  }

  private void cacheInputStream() throws IOException {
    /* Cache the inputstream in order to read it multiple times. For
     * convenience, I use apache.commons IOUtils
     */
    cachedBytes = new ByteArrayOutputStream();
    IOUtils.copy(super.getInputStream(), cachedBytes);
  }

  /* An inputstream which reads the cached request body */
  public class CachedServletInputStream extends ServletInputStream {
    private ByteArrayInputStream input;

    public CachedServletInputStream() {
      /* create a new input stream from the cached request body */
      input = new ByteArrayInputStream(cachedBytes.toByteArray());
    }

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

ตอนนี้สามารถอ่านเนื้อหาคำขอได้มากกว่าหนึ่งครั้งโดยการตัดคำขอเดิมก่อนที่จะส่งผ่านห่วงโซ่ตัวกรอง:

public class MyFilter implements Filter {
  @Override
  public void doFilter(ServletRequest request, ServletResponse response,
        FilterChain chain) throws IOException, ServletException {

    /* wrap the request in order to read the inputstream multiple times */
    MultiReadHttpServletRequest multiReadRequest = new MultiReadHttpServletRequest((HttpServletRequest) request);

    /* here I read the inputstream and do my thing with it; when I pass the
     * wrapped request through the filter chain, the rest of the filters, and
     * request handlers may read the cached inputstream
     */
    doMyThing(multiReadRequest.getInputStream());
    //OR
    anotherUsage(multiReadRequest.getReader());
    chain.doFilter(multiReadRequest, response);
  }
}

การแก้ปัญหานี้ยังจะช่วยให้คุณสามารถอ่านเนื้อหาคำขอหลายครั้งผ่านทางgetParameterXXXวิธีการเพราะโทรอ้างอิงคือซึ่งจะแน่นอนอ่านคำขอที่เก็บไว้getInputStream()InputStream

แก้ไข

สำหรับServletInputStreamอินเทอร์เฟซเวอร์ชันใหม่กว่า คุณจำเป็นต้องให้ดำเนินการตามวิธีอีกไม่กี่เช่นisReady, setReadListenerฯลฯ อ้างถึงนี้คำถามที่บัญญัติไว้ในความคิดเห็นด้านล่าง


5
เป็นเช่นนั้นจริงหรือ? การเรียกที่อ้างอิงคือ getInputStream () ตามคำขอเดิมซึ่งคุณจะได้อ่านไบต์แล้ว คำขอพื้นฐานไม่มีความรู้เกี่ยวกับ Wrapper ของคุณดังนั้นจะรู้ได้อย่างไรว่าจะเรียก getInputStream () ของ Wrapper
Frans

1
เพื่อให้ถูกต้องgetInputStreamถูกเรียกใช้บนwrapper ของฉันเนื่องจากนี่คือServletRequestอินสแตนซ์ที่ฉันผ่านเข้าไปในห่วงโซ่ตัวกรอง หากคุณยังสงสัยโปรดอ่านซอร์สโค้ดServletRequestWrapperและServletRequestอินเทอร์เฟซ
pestrella

1
ถ้าฉันทำมันได้ +100 ฉันจะทำ ฉันพยายามทำให้มันทำงานได้ถูกต้องเป็นเวลา 3-4 ชม. ขอบคุณสำหรับตัวอย่างและคำอธิบายที่ชัดเจน! ฉันดีใจที่พบโพสต์นี้!
ดั๊ก

21
ข้อเสนอแนะใด ๆ ที่จะทำให้ใช้งานกับ Servlet-api 3.0+ ได้อย่างไร ServletInputStream isReady()ตอนนี้มีนามธรรม isFinished()และsetReadListener()จัดการกับ IO ที่ไม่ปิดกั้นซึ่งจะต้องดำเนินการ ฉันคิด ReadListener อาจจะเว้นว่าง แต่ไม่แน่ใจว่าจะทำอย่างไรเกี่ยวกับและisFinished() / หรือ isReady()
Eric B.

12
@ เอริกบี. ขอบคุณอย่างไรก็ตาม ต่อมาฉันพบวิธีแก้ปัญหาสำหรับอินเทอร์เฟซ api ที่ใหม่กว่าซึ่งเพิ่งวางที่นี่ในกรณีที่มีคนสนใจ stackoverflow.com/questions/29208456/…
dcc

37

ฉันรู้ว่าฉันมาช้า แต่คำถามนี้ยังคงเกี่ยวข้องสำหรับฉันและโพสต์ SO นี้เป็นหนึ่งในรายการยอดนิยมใน Google ฉันจะโพสต์วิธีแก้ปัญหาด้วยความหวังว่าจะมีคนช่วยประหยัดเวลาได้สองสามชั่วโมง

ในกรณีของฉันฉันต้องบันทึกคำขอและการตอบกลับทั้งหมดด้วยร่างกายของพวกเขา ใช้ Spring Framework ของคำตอบที่เป็นจริงค่อนข้างง่ายเพียงแค่ใช้ContentCachingRequestWrapperและContentCachingResponseWrapper

import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class LoggingFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void destroy() {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);

        try {
            chain.doFilter(requestWrapper, responseWrapper);
        } finally {

            String requestBody = new String(requestWrapper.getContentAsByteArray());
            String responseBody = new String(responseWrapper.getContentAsByteArray());
            // Do not forget this line after reading response content or actual response will be empty!
            responseWrapper.copyBodyToResponse();

            // Write request and response body, headers, timestamps etc. to log files

        }

    }

}

3
สิ่งนี้ไม่ได้ผลสำหรับฉัน ทั้งคู่requestBodyและresponseBodyเป็นสายที่ว่างเปล่า
Abhijith Madhav

5
ความผิดพลาดของฉัน. ฉันทำchain.doFilter(request, response);แทน achain.doFilter(requestWrapper, responseWrapper);
Abhijith Madhav

5
ContentCaching*Wrapperเรียนมีราคาแพงของการบริโภคสตรีมใส่ดังนั้น "แคช" จะทำผ่านวิธีการgetContentAsByteArrayแต่ระดับนี้ไม่ได้แคชสตรีมใส่ที่อาจจะมีความจำเป็นโดยตัวกรองอื่น ๆ ในห่วงโซ่ตัวกรอง (ซึ่งเป็นกรณีการใช้งานของฉัน) Imho นี่ไม่ใช่พฤติกรรมที่คาดหวังของคลาสแคชเนื้อหาดังนั้นฉันจึงยกการปรับปรุงนี้ในทีมฤดูใบไม้ผลิjira.spring.io/browse/SPR-16028
Federico Piazza

คุณสามารถใช้ได้AbstractRequestLoggingFilterจาก Spring ซึ่งงานส่วนใหญ่เสร็จสิ้นแล้วโดย Spring และคุณต้องลบล้างวิธีง่ายๆเพียง 1 หรือ 2 วิธีเท่านั้น
รุนแรง

1
สิ่งนี้ไม่ได้ผลสำหรับฉัน ณspring-web-4.3.12.RELEASEวันนี้ เมื่อฉันตรวจสอบแหล่งที่มาฉันพบว่าcachedContentมีการใช้ตัวแปรเพื่อเก็บเนื้อหาต่างๆเช่นพารามิเตอร์คำขอและคำขออินพุตสตรีม ว่างเปล่าถ้าคุณโทรมาgetContentAsByteArray()แต่เพียงผู้เดียว getInputStream()ที่จะได้รับการร้องขอของร่างกายที่คุณต้องโทร แต่อีกครั้งสิ่งนี้จะทำให้ inputStream ไม่พร้อมใช้งานกับตัวกรองอื่น ๆ และตัวจัดการ
Ivan Huang

7

คำตอบข้างต้นมีประโยชน์มาก แต่ก็ยังมีปัญหาในประสบการณ์ของฉัน ใน tomcat 7 servlet 3.0 จะต้องเขียนทับ getParamter และ getParamterValues ​​ด้วย โซลูชันในที่นี้มีทั้งพารามิเตอร์ get-query และ post-body ช่วยให้รับสตริงดิบได้อย่างง่ายดาย

เช่นเดียวกับโซลูชันอื่น ๆ ที่ใช้ Apache commons-io และ Googles Guava

ในวิธีนี้เมธอด getParameter * ไม่โยน IOException แต่ใช้ super.getInputStream () (เพื่อรับเนื้อความ) ซึ่งอาจทำให้ IOException ฉันจับมันและโยน runtimeException มันไม่ค่อยดีนัก

import com.google.common.collect.Iterables;
import com.google.common.collect.ObjectArrays;

import org.apache.commons.io.IOUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.entity.ContentType;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

/**
 * Purpose of this class is to make getParameter() return post data AND also be able to get entire
 * body-string. In native implementation any of those two works, but not both together.
 */
public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
    public static final String UTF8 = "UTF-8";
    public static final Charset UTF8_CHARSET = Charset.forName(UTF8);
    private ByteArrayOutputStream cachedBytes;
    private Map<String, String[]> parameterMap;

    public MultiReadHttpServletRequest(HttpServletRequest request) {
        super(request);
    }

    public static void toMap(Iterable<NameValuePair> inputParams, Map<String, String[]> toMap) {
        for (NameValuePair e : inputParams) {
            String key = e.getName();
            String value = e.getValue();
            if (toMap.containsKey(key)) {
                String[] newValue = ObjectArrays.concat(toMap.get(key), value);
                toMap.remove(key);
                toMap.put(key, newValue);
            } else {
                toMap.put(key, new String[]{value});
            }
        }
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (cachedBytes == null) cacheInputStream();
        return new CachedServletInputStream();
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    private void cacheInputStream() throws IOException {
    /* Cache the inputStream in order to read it multiple times. For
     * convenience, I use apache.commons IOUtils
     */
        cachedBytes = new ByteArrayOutputStream();
        IOUtils.copy(super.getInputStream(), cachedBytes);
    }

    @Override
    public String getParameter(String key) {
        Map<String, String[]> parameterMap = getParameterMap();
        String[] values = parameterMap.get(key);
        return values != null && values.length > 0 ? values[0] : null;
    }

    @Override
    public String[] getParameterValues(String key) {
        Map<String, String[]> parameterMap = getParameterMap();
        return parameterMap.get(key);
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        if (parameterMap == null) {
            Map<String, String[]> result = new LinkedHashMap<String, String[]>();
            decode(getQueryString(), result);
            decode(getPostBodyAsString(), result);
            parameterMap = Collections.unmodifiableMap(result);
        }
        return parameterMap;
    }

    private void decode(String queryString, Map<String, String[]> result) {
        if (queryString != null) toMap(decodeParams(queryString), result);
    }

    private Iterable<NameValuePair> decodeParams(String body) {
        Iterable<NameValuePair> params = URLEncodedUtils.parse(body, UTF8_CHARSET);
        try {
            String cts = getContentType();
            if (cts != null) {
                ContentType ct = ContentType.parse(cts);
                if (ct.getMimeType().equals(ContentType.APPLICATION_FORM_URLENCODED.getMimeType())) {
                    List<NameValuePair> postParams = URLEncodedUtils.parse(IOUtils.toString(getReader()), UTF8_CHARSET);
                    params = Iterables.concat(params, postParams);
                }
            }
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
        return params;
    }

    public String getPostBodyAsString() {
        try {
            if (cachedBytes == null) cacheInputStream();
            return cachedBytes.toString(UTF8);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /* An inputStream which reads the cached request body */
    public class CachedServletInputStream extends ServletInputStream {
        private ByteArrayInputStream input;

        public CachedServletInputStream() {
            /* create a new input stream from the cached request body */
            input = new ByteArrayInputStream(cachedBytes.toByteArray());
        }

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

    @Override
    public String toString() {
        String query = dk.bnr.util.StringUtil.nullToEmpty(getQueryString());
        StringBuilder sb = new StringBuilder();
        sb.append("URL='").append(getRequestURI()).append(query.isEmpty() ? "" : "?" + query).append("', body='");
        sb.append(getPostBodyAsString());
        sb.append("'");
        return sb.toString();
    }
}

นี่มันเยี่ยมมาก! ฉันพยายามหาสิ่งนี้มาหลายวันแล้วและใช้ได้กับ servlet 3.1 หนึ่งคำถาม: ทำไมคุณทำdecode(getPostBodyAsString(), result);ในgetParameterMap()? ที่สร้างพารามิเตอร์ด้วย key = request body และ value = null ซึ่งค่อนข้างแปลก
orlade

แทนที่จะไปผ่านทุกแยกสตริงทำไมคุณไม่โทรsuper.getParameterMap()ในของคุณgetParameterMap? ซึ่งจะทำให้คุณมีแผนที่<String, String[]>อย่างไรก็ตาม
Ean V

6

วิธีเดียวคือให้คุณใช้อินพุตสตรีมทั้งหมดในตัวกรองรับสิ่งที่คุณต้องการจากนั้นสร้าง InputStream ใหม่สำหรับเนื้อหาที่คุณอ่านและใส่ InputStream นั้นใน ServletRequestWrapper (หรือ HttpServletRequestWrapper)

ข้อเสียคือคุณจะต้องแยกวิเคราะห์เพย์โหลดด้วยตัวเองมาตรฐานไม่ได้ทำให้ความสามารถนั้นพร้อมใช้งานสำหรับคุณ

ภาคผนวก -

อย่างที่บอกคุณต้องดูที่ HttpServletRequestWrapper

ในตัวกรองคุณดำเนินการต่อโดยเรียกใช้ FilterChain.doFilter (คำขอการตอบกลับ)

สำหรับตัวกรองเล็กน้อยคำขอและการตอบกลับจะเหมือนกับตัวกรองที่ส่งผ่านไปยังตัวกรอง ไม่จำเป็นต้องเป็นเช่นนั้น คุณสามารถแทนที่ด้วยคำขอและ / หรือคำตอบของคุณเอง

HttpServletRequestWrapper ได้รับการออกแบบมาโดยเฉพาะเพื่ออำนวยความสะดวกนี้ คุณส่งคำขอเดิมจากนั้นคุณสามารถดักฟังทุกสายได้ คุณสร้างคลาสย่อยของคุณเองจากสิ่งนี้และแทนที่เมธอด getInputStream ด้วยวิธีการของคุณเอง คุณไม่สามารถเปลี่ยนอินพุตสตรีมของคำขอเดิมได้ดังนั้นคุณจึงมี Wrapper นี้และส่งคืนอินพุตของคุณเอง

กรณีที่ง่ายที่สุดคือใช้สตรีมอินพุตคำขอดั้งเดิมในบัฟเฟอร์ไบต์ทำเวทมนตร์อะไรก็ได้ที่คุณต้องการจากนั้นสร้าง ByteArrayInputStream ใหม่จากบัฟเฟอร์นั้น นี่คือสิ่งที่ส่งคืนใน Wrapper ของคุณซึ่งถูกส่งไปยังเมธอด FilterChain.doFilter

คุณจะต้องซับคลาส ServletInputStream และสร้าง Wrapper อีกอันสำหรับ ByteArrayInputStream ของคุณ แต่นั่นก็ไม่ใช่เรื่องใหญ่เช่นกัน


ฉันไม่สามารถจัดการเพื่ออ่าน InputStream และเรียกคืนหลังจากนั้นไม่มีวิธีรับ / ตั้งค่าเพื่อเข้าถึงสตรีมโดยตรง ข้อเสนอของคุณดูเหมือนจะดี แต่ฉันไม่เห็นวิธีการนำไปใช้
amuniz

5

ฉันก็มีปัญหาเดียวกันเช่นกันและฉันเชื่อว่าโค้ดด้านล่างนี้ง่ายกว่าและใช้ได้ผลสำหรับฉัน

public class MultiReadHttpServletRequest extends  HttpServletRequestWrapper {

 private String _body;

public MultiReadHttpServletRequest(HttpServletRequest request) throws IOException {
   super(request);
   _body = "";
   BufferedReader bufferedReader = request.getReader();           
   String line;
   while ((line = bufferedReader.readLine()) != null){
       _body += line;
   }
}

@Override
public ServletInputStream getInputStream() throws IOException {
   final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(_body.getBytes());
   return new ServletInputStream() {
       public int read() throws IOException {
           return byteArrayInputStream.read();
       }
   };
}

@Override
public BufferedReader getReader() throws IOException {
   return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}

ในคลาส java ตัวกรอง

HttpServletRequest properRequest = ((HttpServletRequest) req);
MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(properRequest);
req = wrappedRequest;
inputJson = IOUtils.toString(req.getReader());
System.out.println("body"+inputJson);

โปรดแจ้งให้เราทราบหากคุณมีข้อสงสัยใด ๆ


3

ดังนั้นนี่จึงเป็นคำตอบของ Lathy แต่ได้รับการอัปเดตสำหรับข้อกำหนดที่ใหม่กว่าสำหรับ ServletInputStream

ได้แก่ (สำหรับ ServletInputStream) เราต้องดำเนินการ:

public abstract boolean isFinished();

public abstract boolean isReady();

public abstract void setReadListener(ReadListener var1);

นี่คือวัตถุของ Lathy ที่แก้ไขแล้ว

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class RequestWrapper extends HttpServletRequestWrapper {

    private String _body;

    public RequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        _body = "";
        BufferedReader bufferedReader = request.getReader();
        String line;
        while ((line = bufferedReader.readLine()) != null){
            _body += line;
        }
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {

        CustomServletInputStream kid = new CustomServletInputStream(_body.getBytes());
        return kid;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
}

และบางแห่ง (??) ฉันพบสิ่งนี้ (ซึ่งเป็นคลาสชั้นหนึ่งที่เกี่ยวข้องกับวิธีการ "พิเศษ"

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

public class CustomServletInputStream extends ServletInputStream {

    private byte[] myBytes;

    private int lastIndexRetrieved = -1;
    private ReadListener readListener = null;

    public CustomServletInputStream(String s) {
        try {
            this.myBytes = s.getBytes("UTF-8");
        } catch (UnsupportedEncodingException ex) {
            throw new IllegalStateException("JVM did not support UTF-8", ex);
        }
    }

    public CustomServletInputStream(byte[] inputBytes) {
        this.myBytes = inputBytes;
    }

    @Override
    public boolean isFinished() {
        return (lastIndexRetrieved == myBytes.length - 1);
    }

    @Override
    public boolean isReady() {
        // This implementation will never block
        // We also never need to call the readListener from this method, as this method will never return false
        return isFinished();
    }

    @Override
    public void setReadListener(ReadListener readListener) {
        this.readListener = readListener;
        if (!isFinished()) {
            try {
                readListener.onDataAvailable();
            } catch (IOException e) {
                readListener.onError(e);
            }
        } else {
            try {
                readListener.onAllDataRead();
            } catch (IOException e) {
                readListener.onError(e);
            }
        }
    }

    @Override
    public int read() throws IOException {
        int i;
        if (!isFinished()) {
            i = myBytes[lastIndexRetrieved + 1];
            lastIndexRetrieved++;
            if (isFinished() && (readListener != null)) {
                try {
                    readListener.onAllDataRead();
                } catch (IOException ex) {
                    readListener.onError(ex);
                    throw ex;
                }
            }
            return i;
        } else {
            return -1;
        }
    }
};

ในที่สุดฉันก็แค่พยายามบันทึกคำขอ และ frankensteined ข้างต้นช่วยให้ฉันสร้างด้านล่าง

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;

//one or the other based on spring version
//import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorAttributes;

import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.filter.OncePerRequestFilter;


/**
 * A filter which logs web requests that lead to an error in the system.
 */
@Component
public class LogRequestFilter extends OncePerRequestFilter implements Ordered {

    // I tried apache.commons and slf4g loggers.  (one or the other in these next 2 lines of declaration */
    //private final static org.apache.commons.logging.Log logger = org.apache.commons.logging.LogFactory.getLog(LogRequestFilter.class);
    private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(LogRequestFilter.class);

    // put filter at the end of all other filters to make sure we are processing after all others
    private int order = Ordered.LOWEST_PRECEDENCE - 8;
    private ErrorAttributes errorAttributes;

    @Override
    public int getOrder() {
        return order;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        String temp = ""; /* for a breakpoint, remove for production/real code */

        /* change to true for easy way to comment out this code, remove this if-check for production/real code */
        if (false) {
            filterChain.doFilter(request, response);
            return;
        }

        /* make a "copy" to avoid issues with body-can-only-read-once issues */
        RequestWrapper reqWrapper = new RequestWrapper(request);

        int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
        // pass through filter chain to do the actual request handling
        filterChain.doFilter(reqWrapper, response);
        status = response.getStatus();

        try {
            Map<String, Object> traceMap = getTrace(reqWrapper, status);
            // body can only be read after the actual request handling was done!
            this.getBodyFromTheRequestCopy(reqWrapper, traceMap);

            /* now do something with all the pieces of information gatherered */
            this.logTrace(reqWrapper, traceMap);
        } catch (Exception ex) {
            logger.error("LogRequestFilter FAILED: " + ex.getMessage(), ex);
        }
    }

    private void getBodyFromTheRequestCopy(RequestWrapper rw, Map<String, Object> trace) {
        try {
            if (rw != null) {
                byte[] buf = IOUtils.toByteArray(rw.getInputStream());
                //byte[] buf = rw.getInputStream();
                if (buf.length > 0) {
                    String payloadSlimmed;
                    try {
                        String payload = new String(buf, 0, buf.length, rw.getCharacterEncoding());
                        payloadSlimmed = payload.trim().replaceAll(" +", " ");
                    } catch (UnsupportedEncodingException ex) {
                        payloadSlimmed = "[unknown]";
                    }

                    trace.put("body", payloadSlimmed);
                }
            }
        } catch (IOException ioex) {
            trace.put("body", "EXCEPTION: " + ioex.getMessage());
        }
    }

    private void logTrace(HttpServletRequest request, Map<String, Object> trace) {
        Object method = trace.get("method");
        Object path = trace.get("path");
        Object statusCode = trace.get("statusCode");

        logger.info(String.format("%s %s produced an status code '%s'. Trace: '%s'", method, path, statusCode,
                trace));
    }

    protected Map<String, Object> getTrace(HttpServletRequest request, int status) {
        Throwable exception = (Throwable) request.getAttribute("javax.servlet.error.exception");

        Principal principal = request.getUserPrincipal();

        Map<String, Object> trace = new LinkedHashMap<String, Object>();
        trace.put("method", request.getMethod());
        trace.put("path", request.getRequestURI());
        if (null != principal) {
            trace.put("principal", principal.getName());
        }
        trace.put("query", request.getQueryString());
        trace.put("statusCode", status);

        Enumeration headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String key = (String) headerNames.nextElement();
            String value = request.getHeader(key);
            trace.put("header:" + key, value);
        }

        if (exception != null && this.errorAttributes != null) {
            trace.put("error", this.errorAttributes
                    .getErrorAttributes((WebRequest) new ServletRequestAttributes(request), true));
        }

        return trace;
    }
}

โปรดใช้รหัสนี้พร้อมกับเกลือเม็ดหนึ่ง

"การทดสอบ" ที่สำคัญที่สุดคือถ้า POST ทำงานกับเพย์โหลด นี่คือสิ่งที่จะเปิดโปงประเด็น "อ่านซ้ำ"

รหัสตัวอย่างหลอก

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("myroute")
public class MyController {
    @RequestMapping(method = RequestMethod.POST, produces = "application/json")
    @ResponseBody
    public String getSomethingExample(@RequestBody MyCustomObject input) {

        String returnValue = "";

        return returnValue;
    }
}

คุณสามารถแทนที่ "MyCustomObject" ด้วย "Object" ธรรมดาได้หากต้องการทดสอบ

คำตอบนี้ได้รับการออกแบบมาจากโพสต์และตัวอย่าง SOF ที่แตกต่างกันหลายแห่ง .. แต่ใช้เวลาสักพักในการดึงทั้งหมดเข้าด้วยกันดังนั้นฉันหวังว่ามันจะช่วยผู้อ่านในอนาคตได้

โปรดโหวตคำตอบของ Lathy ก่อนของฉัน ฉันไม่สามารถไปได้ไกลหากไม่มีมัน

ด้านล่างนี้เป็นหนึ่ง / ข้อยกเว้นบางประการที่ฉันได้รับในขณะที่ดำเนินการนี้

getReader () ถูกเรียกสำหรับคำขอนี้แล้ว

ดูเหมือนว่าสถานที่บางแห่งที่ฉัน "ยืมมา" จะอยู่ที่นี่:

http://slackspace.de/articles/log-request-body-with-spring-boot/

https://github.com/c0nscience/spring-web-logging/blob/master/src/main/java/org/zalando/springframework/web/logging/LoggingFilter.java

https://howtodoinjava.com/servlets/httpservletrequestwrapper-example-read-request-body/

https://www.oodlestechnologies.com/blogs/How-to-create-duplicate-object-of-httpServletRequest-object

https://github.com/c0nscience/spring-web-logging/blob/master/src/main/java/org/zalando/springframework/web/logging/LoggingFilter.java


3

Spring มีการรองรับในตัวสำหรับสิ่งนี้ด้วยAbstractRequestLoggingFilter:

@Bean
public Filter loggingFilter(){
    final AbstractRequestLoggingFilter filter = new AbstractRequestLoggingFilter() {
        @Override
        protected void beforeRequest(final HttpServletRequest request, final String message) {

        }

        @Override
        protected void afterRequest(final HttpServletRequest request, final String message) {

        }
    };

    filter.setIncludePayload(true);
    filter.setIncludeQueryString(false);
    filter.setMaxPayloadLength(1000000);

    return filter;
}

น่าเสียดายที่คุณยังไม่สามารถอ่านเพย์โหลดจากคำขอได้โดยตรง แต่พารามิเตอร์ข้อความ String จะรวมส่วนของข้อมูลเพื่อให้คุณสามารถดึงข้อมูลได้จากที่นั่นดังนี้:

String body = message.substring(message.indexOf("{"), message.lastIndexOf("]"));


ฉันหวังว่าจะใช้โซลูชันของคุณเพื่อสร้างบันทึกการตรวจสอบ แต่ฉันต้องการบันทึกว่าคำขอสำเร็จหรือไม่ฉันสามารถเชื่อมต่อกับการตอบกลับ http และรับรหัสภายในคลาสนี้ได้หรือไม่
jonesy

1

แค่เขียนทับgetInputStream()ไม่ได้ผลในกรณีของฉัน การใช้งานเซิร์ฟเวอร์ของฉันดูเหมือนจะแยกวิเคราะห์พารามิเตอร์โดยไม่เรียกใช้เมธอดนี้ ฉันไม่พบวิธีอื่นใด แต่นำเมธอด getParameter * ทั้งสี่มาใช้ใหม่เช่นกัน นี่คือรหัสของgetParameterMap(ไคลเอ็นต์ Apache Http และไลบรารี Google Guava ที่ใช้):

@Override
public Map<String, String[]> getParameterMap() {
    Iterable<NameValuePair> params = URLEncodedUtils.parse(getQueryString(), NullUtils.UTF8);

    try {
        String cts = getContentType();
        if (cts != null) {
            ContentType ct = ContentType.parse(cts);
            if (ct.getMimeType().equals(ContentType.APPLICATION_FORM_URLENCODED.getMimeType())) {
                List<NameValuePair> postParams = URLEncodedUtils.parse(IOUtils.toString(getReader()), NullUtils.UTF8);
                params = Iterables.concat(params, postParams);
            }
        }
    } catch (IOException e) {
        throw new IllegalStateException(e);
    }
    Map<String, String[]> result = toMap(params);
    return result;
}

public static Map<String, String[]> toMap(Iterable<NameValuePair> body) {
    Map<String, String[]> result = new LinkedHashMap<>();
    for (NameValuePair e : body) {
        String key = e.getName();
        String value = e.getValue();
        if (result.containsKey(key)) {
            String[] newValue = ObjectArrays.concat(result.get(key), value);
            result.remove(key);
            result.put(key, newValue);
        } else {
            result.put(key, new String[] {value});
        }
    }
    return result;
}

1
Jetty มีปัญหานี้ขออภัยgrepcode.com/file/repo1.maven.org/maven2/org.eclipse.jetty/…
mikeapr4

คุณอาจใช้ Tomcat 7 ขึ้นไปกับ Servlet 3.0? คุณมีรหัสสำหรับอีก 3 วิธีด้วยหรือไม่?
เงิน

อีก 3 วิธีเพียงแค่เรียก getParameterMap () และดึงค่าที่ต้องการ
30

0

หากคุณมีการควบคุมมากกว่าการร้องขอคุณสามารถตั้งค่าชนิดเนื้อหาที่จะไบนารี / octet สตรีม สิ่งนี้ช่วยให้สามารถค้นหาพารามิเตอร์โดยไม่ต้องใช้อินพุตสตรีม

อย่างไรก็ตามสิ่งนี้อาจเฉพาะสำหรับแอปพลิเคชันเซิร์ฟเวอร์บางตัว ผมทดสอบเฉพาะคราว, ท่าเทียบเรือดูเหมือนว่าจะทำงานในลักษณะเดียวกันตามhttps://stackoverflow.com/a/11434646/957103


0

เมธอด getContentAsByteArray () ของคลาส Spring ContentCachingRequestWrapper อ่านเนื้อความหลายครั้ง แต่เมธอด getInputStream () และ getReader () ของคลาสเดียวกันไม่อ่านเนื้อความหลายครั้ง:

"คลาสนี้แคชเนื้อหาคำขอโดยใช้ InputStream หากเราอ่าน InputStream ในตัวกรองตัวใดตัวหนึ่งตัวกรองอื่น ๆ ที่ตามมาในห่วงโซ่ตัวกรองจะไม่สามารถอ่านได้อีกต่อไปเนื่องจากข้อ จำกัด นี้คลาสนี้จึงไม่เหมาะสมทั้งหมด สถานการณ์ "

ในกรณีของฉันวิธีแก้ปัญหาทั่วไปที่แก้ไขปัญหานี้คือการเพิ่มสามคลาสต่อไปนี้ในโครงการ Spring boot ของฉัน (และการอ้างอิงที่จำเป็นไปยังไฟล์ pom):

CachedBodyHttpServletRequest.java:

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);
    }

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

    @Override
    public BufferedReader getReader() throws IOException {
        // Create a reader from cachedContent
        // and return it
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
        return new BufferedReader(new InputStreamReader(byteArrayInputStream));
    }
}

CachedBodyServletInputStream.java:

public class CachedBodyServletInputStream extends ServletInputStream {

    private InputStream cachedBodyInputStream;

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

    @Override
    public boolean isFinished() {
        try {
            return cachedBodyInputStream.available() == 0;
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return false;
    }

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

    @Override
    public void setReadListener(ReadListener readListener) {
        throw new UnsupportedOperationException();
    }

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

ContentCachingFilter.java:

@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Component
@WebFilter(filterName = "ContentCachingFilter", urlPatterns = "/*")
public class ContentCachingFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        System.out.println("IN  ContentCachingFilter ");
        CachedBodyHttpServletRequest cachedBodyHttpServletRequest = new CachedBodyHttpServletRequest(httpServletRequest);
        filterChain.doFilter(cachedBodyHttpServletRequest, httpServletResponse);
    }
}

ฉันยังเพิ่มการอ้างอิงต่อไปนี้ให้กับ pom:

<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>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.10.0</version>
</dependency>

ซอร์สโค้ดสำหรับการสอนและแบบเต็มอยู่ที่นี่: https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times


0

ฉันพบทางออกที่ดีสำหรับเนื้อหาคำขอทุกรูปแบบ ฉันทดสอบapplication/x-www-form-urlencodedและapplication/jsonทั้งสองได้ผลดีมาก ปัญหาContentCachingRequestWrapperนั้นออกแบบมาสำหรับx-www-form-urlencodedตัวขอเท่านั้น แต่ใช้ไม่ได้กับเช่น json ผมพบว่าวิธีแก้ปัญหาสำหรับ JSON การเชื่อมโยง x-www-form-urlencodedมันมีปัญหาว่ามันไม่ได้สนับสนุน ฉันเข้าร่วมทั้งสองในรหัสของฉัน:

import org.apache.commons.io.IOUtils;
import org.springframework.web.util.ContentCachingRequestWrapper;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class MyContentCachingRequestWrapper extends ContentCachingRequestWrapper {

    private byte[] body;

    public MyContentCachingRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        super.getParameterMap(); // init cache in ContentCachingRequestWrapper
        body = super.getContentAsByteArray(); // first option for application/x-www-form-urlencoded
        if (body.length == 0) {
            body = IOUtils.toByteArray(super.getInputStream()); // second option for other body formats
        }
    }

    public byte[] getBody() {
        return body;
    }

    @Override
    public ServletInputStream getInputStream() {
        return new RequestCachingInputStream(body);
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
    }

    private static class RequestCachingInputStream extends ServletInputStream {

        private final ByteArrayInputStream inputStream;

        public RequestCachingInputStream(byte[] bytes) {
            inputStream = new ByteArrayInputStream(bytes);
        }

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

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

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

        @Override
        public void setReadListener(ReadListener readlistener) {
        }

    }

}

-1

คุณสามารถใช้โซ่ตัวกรอง servlet แต่ใช้โซ่เดิมแทนคุณสามารถสร้างคำขอของคุณเองได้


1
ดูเหมือนว่าลิงก์ไปยังบทช่วยสอนจะมีไวรัสอยู่
fasth

-2

ก่อนอื่นเราไม่ควรอ่านพารามิเตอร์ภายในตัวกรอง โดยปกติส่วนหัวจะถูกอ่านในตัวกรองเพื่อดำเนินการตรวจสอบสิทธิ์บางอย่าง ต้องบอกว่าเราสามารถอ่านเนื้อหา HttpRequest ได้อย่างสมบูรณ์ใน Filter หรือ Interceptor โดยใช้ CharStreams:

String body = com.google.common.io.CharStreams.toString(request.getReader());

สิ่งนี้ไม่มีผลต่อการอ่านครั้งต่อ ๆ ไปเลย


ใช่. หากคุณทำสิ่งนี้ครั้งเดียวrequest.getReader()จะส่งคืนโปรแกรมอ่านที่มีเฉพาะสตริงว่างในการอ่านที่ตามมา
christophetd

1
ฉันจะทำงานในกรณีที่เขียนทับเมธอด getReader () และ getInputStream () เพื่อใช้เนื้อหาใหม่นี้เป็นแหล่งที่มา
Rodrigo Borba
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.