web-dev-qa-db-ja.com

REST template Java Spring MVCを使用してサーバーから大きなファイルをダウンロードする

RESTサービスを使用して大きなISOファイルを送信します。RESTサービスに問題はありません。これで、ファイルを取得する残りのサービス、クライアント(Webアプリ)側でメモリ不足の例外を受け取ります。以下は私のコードです

HttpHeaders headers = new HttpHeaders();//1 Line

    headers.setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM));//2 Line
    headers.set("Content-Type","application/json");//3 Line
    headers.set("Cookie", "session=abc");//4 Line
    HttpEntity statusEntity=new HttpEntity(headers);//5 Line
    String uri_status=new String("http://"+ip+":8080/pcap/file?fileName={name}");//6 Line

    ResponseEntity<byte[]>resp_status=rt.exchange(uri_status, HttpMethod.GET, statusEntity, byte[].class,"File5.iso");//7 Line

私は7行でメモリ不足の例外を受け取ります。バッファリングして部分的に取得する必要がありますが、サーバーからこのファイルを取得する方法はわかりません、ファイルのサイズは約500から700MBです。誰でも助けてください。

例外スタック:

  org.springframework.web.util.NestedServletException: Handler processing failed; nested exception is Java.lang.OutOfMemoryError: Java heap space
    org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.Java:972)
    org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.Java:852)
    org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.Java:882)
    org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.Java:778)
    javax.servlet.http.HttpServlet.service(HttpServlet.Java:622)
    javax.servlet.http.HttpServlet.service(HttpServlet.Java:729)
    org.Apache.Tomcat.websocket.server.WsFilter.doFilter(WsFilter.Java:52)
root cause

Java.lang.OutOfMemoryError: Java heap space
    Java.util.Arrays.copyOf(Arrays.Java:3236)
    Java.io.ByteArrayOutputStream.grow(ByteArrayOutputStream.Java:118)
    Java.io.ByteArrayOutputStream.ensureCapacity(ByteArrayOutputStream.Java:93)
    Java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.Java:153)
    org.springframework.util.FileCopyUtils.copy(FileCopyUtils.Java:113)
    org.springframework.util.FileCopyUtils.copyToByteArray(FileCopyUtils.Java:164)
    org.springframework.http.converter.ByteArrayHttpMessageConverter.readInternal(ByteArrayHttpMessageConverter.Java:58)
    org.springframework.http.converter.ByteArrayHttpMessageConverter.readInternal(ByteArrayHttpMessageConverter.Java:1)
    org.springframework.http.converter.AbstractHttpMessageConverter.read(AbstractHttpMessageConverter.Java:153)
    org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.Java:81)
    org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.Java:627)
    org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.Java:1)
    org.springframework.web.client.RestTemplate.doExecute(RestTemplate.Java:454)
    org.springframework.web.client.RestTemplate.execute(RestTemplate.Java:409)
    org.springframework.web.client.RestTemplate.exchange(RestTemplate.Java:385)
    com.pcap.webapp.HomeController.getPcapFile(HomeController.Java:186)

私のサーバー側REST正常に動作しているサービスコードは

@RequestMapping(value = URIConstansts.GET_FILE, produces = { MediaType.APPLICATION_OCTET_STREAM_VALUE}, method = RequestMethod.GET)
public void getFile(@RequestParam(value="fileName", required=false) String fileName,HttpServletRequest request,HttpServletResponse response) throws IOException{



    byte[] reportBytes = null;
    File result=new File("/home/arpit/Documents/PCAP/dummyPath/"+fileName);

    if(result.exists()){
        InputStream inputStream = new FileInputStream("/home/arpit/Documents/PCAP/dummyPath/"+fileName); 
        String type=result.toURL().openConnection().guessContentTypeFromName(fileName);
        response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
        response.setHeader("Content-Type",type);

        reportBytes=new byte[100];//New change
        OutputStream os=response.getOutputStream();//New change
        int read=0;
        while((read=inputStream.read(reportBytes))!=-1){
            os.write(reportBytes,0,read);
        }
        os.flush();
        os.close();






    }
14
arpit joshi

ここに私がそれをする方法があります。このヒントに基づいて Spring Jira issue

RestTemplate restTemplate // = ...;

// Optional Accept header
RequestCallback requestCallback = request -> request.getHeaders()
        .setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));

// Streams the response instead of loading it all in memory
ResponseExtractor<Void> responseExtractor = response -> {
    // Here I write the response to a file but do what you like
    Path path = Paths.get("some/path");
    Files.copy(response.getBody(), path);
    return null;
};
restTemplate.execute(URI.create("www.something.com"), HttpMethod.GET, requestCallback, responseExtractor);

前述のJiraの問題から:

抽出メソッドからInputStreamを単純に返すことはできないことに注意してください。executeメソッドが戻るまでに、基礎となる接続とストリームはすでに閉じられているためです。

Spring 5の更新

Spring 5では、非同期(非ブロッキングなど)http要求を許可する WebClient クラスが導入されました。ドキュメントから:

RestTemplateと比較すると、WebClientは次のとおりです。

  • ノンブロッキング、リアクティブ、少ないハードウェアリソースで高い同時実行性をサポートします。
  • Java 8ラムダを利用する機能APIを提供します。
  • 同期シナリオと非同期シナリオの両方をサポートします。
  • サーバーからのストリーミングをサポートします。
22
bernie

@ bernie のように、WebClientを使用してこれを実現できます。

public void downloadFileUrl( HttpServletResponse response ) throws IOException {

    WebClient webClient = WebClient.create();

    // Request service to get file data
    Flux<DataBuffer> fileDataStream = webClient.get()
            .uri( this.fileUrl )
            .accept( MediaType.APPLICATION_OCTET_STREAM )
            .retrieve()
            .bodyToFlux( DataBuffer.class );

    // Streams the stream from response instead of loading it all in memory
    DataBufferUtils.write( fileDataStream, response.getOutputStream() )
            .map( DataBufferUtils::release )
            .then()
            .block();
}

Reactive ServerスタックがなくてもWebClientを使用できます。RossenStoyanchev(Spring Frameworkチームのメンバー)は、これを Spring MVC開発者向けの「Reactive」ガイド プレゼンテーションで非常によく説明しています。このプレゼンテーションの中で、Rossen StoyanchevはRestTemplateの廃止を検討したと述べましたが、結局彼らは延期することに決めましたが、それは今後も発生する可能性があります

これまでのところWebClientを使用する主な欠点は非常に急な学習曲線(リアクティブプログラミング)ですが、将来回避する方法はないと思うので、後者よりも早く見てみる方が良いでしょう。

上記の正解のより良いバージョンは、以下のコードです。このメソッドは、ダウンロードされた情報の実際のソースとして機能する別のアプリケーションまたはサービスにダウンロード要求を送信します。

public void download(HttpServletRequest req, HttpServletResponse res, String url)
            throws ResourceAccessException, GenericException {
        try {
            logger.info("url::" + url);
            if (restTemplate == null)
                logger.info("******* rest template is null***********************");
            RequestCallback requestCallback = request -> request.getHeaders()
                    .setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));

            // Streams the response instead of loading it all in memory
            ResponseExtractor<ResponseEntity<InputStream>> responseExtractor = response -> {

                String contentDisposition = response.getHeaders().getFirst("Content-Disposition");
                if (contentDisposition != null) {
                    // Temporary location for files that will be downloaded from micro service and
                    // act as final source of download to user
                    String filePath = "/home/devuser/fileupload/download_temp/" + contentDisposition.split("=")[1];
                    Path path = Paths.get(filePath);
                    Files.copy(response.getBody(), path, StandardCopyOption.REPLACE_EXISTING);

                    // Create a new input stream from temporary location and use it for downloading
                    InputStream inputStream = new FileInputStream(filePath);
                    String type = req.getServletContext().getMimeType(filePath);
                    res.setHeader("Content-Disposition", "attachment; filename=" + contentDisposition.split("=")[1]);
                    res.setHeader("Content-Type", type);

                    byte[] outputBytes = new byte[100];
                    OutputStream os = res.getOutputStream();
                    int read = 0;
                    while ((read = inputStream.read(outputBytes)) != -1) {
                        os.write(outputBytes, 0, read);
                    }
                    os.flush();
                    os.close();
                    inputStream.close();
                }
                return null;
            };
            restTemplate.execute(url, HttpMethod.GET, requestCallback, responseExtractor);
        } catch (Exception e) {
            logger.info(e.toString());
            throw e;
        }
    }
0
Rajdeep

これにより、リクエスト全体がメモリにロードされなくなります。

SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setBufferRequestBody(false);
RestTemplate rest = new RestTemplate(requestFactory);

Java.lang.OutOfMemoryErrorの場合:Javaヒープスペースは、JVMにメモリを追加することで解決できます。

-Xmxnメモリ割り当てプールの最大サイズをバイト単位で指定します。この値は、2 MBより大きい1024の倍数でなければなりません。キロバイトを示すには文字kまたはKを追加し、メガバイトを示すにはmまたはMを追加します。デフォルト値は、システム構成に基づいて実行時に選択されます。

サーバーの展開では、-Xmsと-Xmxは多くの場合同じ値に設定されます。 http://docs.Oracle.com/javase/7/docs/technotes/guides/vm/gc-ergonomics.html のガベージコレクターの人間工学を参照してください。

例:

-Xmx83886080
-Xmx81920k
-Xmx80m

おそらく、あなたが抱えている問題は、実行しようとしているリクエスト(大きなファイルをダウンロードする)に厳密には関係していませんが、プロセスに割り当てられたメモリが十分ではありません。

0
Andrea Girardi