Pobieranie pliku z kontrolerów wiosennych

387

Mam wymaganie, aby pobrać plik PDF ze strony internetowej. PDF musi zostać wygenerowany w kodzie, który, jak sądzę, będzie kombinacją freemarkera i frameworka generowania PDF, takiego jak iText. Czy jest lepszy sposób?

Jednak moim głównym problemem jest to, jak mogę pozwolić użytkownikowi na pobranie pliku za pomocą kontrolera Spring?

MilindaD
źródło
2
Warto wspomnieć, że Spring Framework bardzo się zmienił od 2011 roku, więc możesz to zrobić również w sposób reaktywny - oto przykład
Krzysztof Skrzynecki

Odpowiedzi:

397
@RequestMapping(value = "/files/{file_name}", method = RequestMethod.GET)
public void getFile(
    @PathVariable("file_name") String fileName, 
    HttpServletResponse response) {
    try {
      // get your file as InputStream
      InputStream is = ...;
      // copy it to response's OutputStream
      org.apache.commons.io.IOUtils.copy(is, response.getOutputStream());
      response.flushBuffer();
    } catch (IOException ex) {
      log.info("Error writing file to output stream. Filename was '{}'", fileName, ex);
      throw new RuntimeException("IOError writing file to output stream");
    }

}

Mówiąc ogólnie, kiedy masz response.getOutputStream(), możesz tam napisać wszystko. Możesz przekazać ten strumień wyjściowy jako miejsce do umieszczenia wygenerowanego pliku PDF w generatorze. Ponadto, jeśli wiesz, jaki typ pliku wysyłasz, możesz ustawić

response.setContentType("application/pdf");
Infeligo
źródło
4
To właściwie to, co chciałem powiedzieć, ale prawdopodobnie powinieneś również ustawić nagłówek typu odpowiedzi na coś odpowiedniego dla pliku.
GaryF
2
Tak, właśnie edytowałem post. Wygenerowałem różne typy plików, więc pozostawiłem to przeglądarce, aby określić typ zawartości pliku według jego rozszerzenia.
Infeligo,
Zapomniałem flushBuffer, dzięki Twojemu wpisowi, zrozumiałem, dlaczego mój nie działa :-)
Jan Vladimir Mostert
35
Czy jest jakiś konkretny powód, aby używać Apache IOUtilszamiast Spring FileCopyUtils?
Władca
3
Oto lepsze rozwiązanie: stackoverflow.com/questions/16652760/…
Dmytro Plekhotkin
290

Byłem w stanie przesłać strumieniowo ten wiersz, używając wbudowanej obsługi wiosną z ResourceHttpMessageConverter. Spowoduje to ustawienie długości i typu zawartości, jeśli będzie w stanie określić typ MIME

@RequestMapping(value = "/files/{file_name}", method = RequestMethod.GET)
@ResponseBody
public FileSystemResource getFile(@PathVariable("file_name") String fileName) {
    return new FileSystemResource(myService.getFileFor(fileName)); 
}
Scott Carlson
źródło
10
To działa. Ale plik (plik .csv) jest wyświetlany w przeglądarce i nie jest pobierany - jak zmusić przeglądarkę do pobrania?
chzbrgla
41
Możesz dodać produkuje = MediaType.APPLICATION_OCTET_STREAM_VALUE do @RequestMapping, aby wymusić pobranie
David Kago,
8
Powinieneś także dodać <bean class = "org.springframework.http.converter.ResourceHttpMessageConverter" /> do listy messageConverters (<mvc: powered by adnotation> <mvc: message-konwertery>)
Sllouyssgort
4
Czy istnieje sposób ustawienia Content-Dispositionnagłówka w ten sposób?
Ralph
8
Nie potrzebowałem tego, ale myślę, że możesz dodać HttpResponse jako parametr do metody, a następnie „response.setHeader („ Content-Disposition ”,„ załącznik; filename = somefile.pdf ”);”
Scott Carlson,
82

Powinieneś być w stanie zapisać plik bezpośrednio w odpowiedzi. Coś jak

response.setContentType("application/pdf");      
response.setHeader("Content-Disposition", "attachment; filename=\"somefile.pdf\""); 

a następnie zapisz plik jako strumień binarny response.getOutputStream(). Pamiętaj, aby zrobić response.flush()na końcu i to powinno wystarczyć.

lobster1234
źródło
8
nie jest sposobem „wiosennym” na ustawienie typu zawartości w ten sposób? @RequestMapping(value = "/foo/bar", produces = "application/pdf")
Czarny
4
@Francis co jeśli Twoja aplikacja pobiera różne typy plików? Odpowiedź Lobster1234 umożliwia dynamiczne ustawienie dyspozycji treści.
Rose
2
to prawda @Rose, ale uważam, że lepszym rozwiązaniem byłoby zdefiniowanie różnych punktów końcowych dla każdego formatu
Black
3
Chyba nie, ponieważ nie jest skalowalny. Obecnie obsługujemy kilkanaście rodzajów zasobów. Możemy obsłużyć więcej typów plików w zależności od tego, co użytkownicy chcą przesłać, w takim przypadku możemy skończyć z tyloma punktami końcowymi, które zasadniczo robią to samo. IMHO musi być tylko jeden punkt końcowy pobierania i obsługuje wiele typów plików. @Francis
Rose
3
jest absolutnie „skalowalny”, ale możemy zgodzić się, że nie jest to najlepsza praktyka
Black
74

W Spring 3.0 możesz użyć HttpEntityobiektu zwracanego. Jeśli tego użyjesz, twój kontroler nie potrzebuje HttpServletResponseobiektu, dlatego łatwiej go przetestować. Poza tym, ta odpowiedź jest względna równa tej z Infeligo .

Jeśli zwracaną wartością twojego frameworka pdf jest tablica bajtów (przeczytaj drugą część mojej odpowiedzi dla innych zwracanych wartości) :

@RequestMapping(value = "/files/{fileName}", method = RequestMethod.GET)
public HttpEntity<byte[]> createPdf(
                 @PathVariable("fileName") String fileName) throws IOException {

    byte[] documentBody = this.pdfFramework.createPdf(filename);

    HttpHeaders header = new HttpHeaders();
    header.setContentType(MediaType.APPLICATION_PDF);
    header.set(HttpHeaders.CONTENT_DISPOSITION,
                   "attachment; filename=" + fileName.replace(" ", "_"));
    header.setContentLength(documentBody.length);

    return new HttpEntity<byte[]>(documentBody, header);
}

Jeśli typ zwracany przez Framework PDF ( documentBbody) nie jest już tablicą bajtów (a także nie ByteArrayInputStream), rozsądnie byłoby NIE uczynić go najpierw tablicą bajtów. Zamiast tego lepiej jest użyć:

przykład z FileSystemResource:

@RequestMapping(value = "/files/{fileName}", method = RequestMethod.GET)
public HttpEntity<byte[]> createPdf(
                 @PathVariable("fileName") String fileName) throws IOException {

    File document = this.pdfFramework.createPdf(filename);

    HttpHeaders header = new HttpHeaders();
    header.setContentType(MediaType.APPLICATION_PDF);
    header.set(HttpHeaders.CONTENT_DISPOSITION,
                   "attachment; filename=" + fileName.replace(" ", "_"));
    header.setContentLength(document.length());

    return new HttpEntity<byte[]>(new FileSystemResource(document),
                                  header);
}
Ralph
źródło
11
-1 to niepotrzebnie załaduje cały plik do pamięci może łatwo wywołać OutOfMemoryErrors.
Faisal Feroz
1
@FaisalFeroz: tak, to prawda, ale dokument pliku i tak jest tworzony w pamięci (patrz pytanie: „PDF należy wygenerować w kodzie”). W każdym razie - jakie jest twoje rozwiązanie problemu?
Ralph
1
Możesz także użyć ResponseEntity, który jest super HttpEntity, który pozwala określić kod statusu odpowiedzi HTTP. Przykład:return new ResponseEntity<byte[]>(documentBody, headers, HttpStatus.CREATED)
Amr Mostafa,
@Amr Mostafa: ResponseEntityjest podklasą HttpEntity(ale rozumiem), z drugiej strony 201 CREATED nie jest tym, czego używałbym, gdy zwracam tylko widok danych. (patrz w3.org/Protocols/rfc2616/rfc2616-sec10.html dla 201 TWORZONY )
Ralph,
1
Czy istnieje powód, dla którego zastępujesz białe znaki podkreśleniem w nazwie pliku? Możesz zawinąć w cudzysłów, aby wysłać rzeczywistą nazwę.
Alexandru Severin
63

Jeśli ty:

  • Nie chcę ładować całego pliku byte[]przed wysłaniem do odpowiedzi;
  • Chcesz / muszę wysłać / pobrać za pośrednictwem InputStream;
  • Chcesz mieć pełną kontrolę nad typem MIME i nazwą pliku;
  • Miej @ControllerAdvicedla ciebie inne wyjątki związane z odbiorem (lub nie).

Poniższy kod jest tym, czego potrzebujesz:

@RequestMapping(value = "/stuff/{stuffId}", method = RequestMethod.GET)
public ResponseEntity<FileSystemResource> downloadStuff(@PathVariable int stuffId)
                                                                      throws IOException {
    String fullPath = stuffService.figureOutFileNameFor(stuffId);
    File file = new File(fullPath);
    long fileLength = file.length(); // this is ok, but see note below

    HttpHeaders respHeaders = new HttpHeaders();
    respHeaders.setContentType("application/pdf");
    respHeaders.setContentLength(fileLength);
    respHeaders.setContentDispositionFormData("attachment", "fileNameIwant.pdf");

    return new ResponseEntity<FileSystemResource>(
        new FileSystemResource(file), respHeaders, HttpStatus.OK
    );
}

O części dotyczącej długości pliku : File#length()powinno być wystarczająco dobre w ogólnym przypadku, ale pomyślałem, że dokonam tej obserwacji, ponieważ może być powolna , w którym to przypadku powinieneś ją wcześniej zapisać (np. W DB). Przypadki mogą być powolne obejmują: jeśli plik jest duży, szczególnie jeśli plik jest w systemie zdalnym lub coś bardziej skomplikowanego - może to być baza danych.



InputStreamResource

Jeśli Twój zasób nie jest plikiem, np. Pobierasz dane z bazy danych, powinieneś użyć InputStreamResource. Przykład:

    InputStreamResource isr = new InputStreamResource(new FileInputStream(file));
    return new ResponseEntity<InputStreamResource>(isr, respHeaders, HttpStatus.OK);
acdcjunior
źródło
Nie radzisz używać klasy FileSystemResource?
Stephane
Właściwie uważam, że korzystanie z nich jest w porządku FileSystemResource. Wskazane jest nawet, jeśli Twój zasób jest plikiem . W tej próbce FileSystemResourcemożna użyć gdzie InputStreamResourcejest.
acdcjunior
O części dotyczącej obliczania długości pliku: jeśli się martwisz, nie rób tego. File#length()powinno być wystarczająco dobre w ogólnym przypadku. Właśnie wspomniałem o tym, ponieważ może być powolny , szczególnie jeśli plik znajduje się w systemie zdalnym lub coś bardziej skomplikowanego - może baza danych? Ale martw się tylko, jeśli stanie się to problemem (lub jeśli masz twarde dowody, że stanie się jednym), a nie wcześniej. Chodzi o to, że: starasz się przesyłać strumieniowo plik, jeśli musisz wcześniej go wstępnie załadować, to strumieniowanie kończy się bez różnicy, prawda?
acdcjunior
dlaczego powyższy kod nie działa dla mnie? Pobiera plik 0 bajtów. Sprawdziłem i upewniłem się, że są tam konwertery ByteArray i ResourceMessage. Czy coś brakuje?
coding_idiot
Dlaczego martwisz się o konwertery ByteArray i ResourceMessage?
acdcjunior
20

Ten kod działa dobrze, aby automatycznie pobrać plik ze sterownika wiosennego po kliknięciu łącza w jsp.

@RequestMapping(value="/downloadLogFile")
public void getLogFile(HttpSession session,HttpServletResponse response) throws Exception {
    try {
        String filePathToBeServed = //complete file name with path;
        File fileToDownload = new File(filePathToBeServed);
        InputStream inputStream = new FileInputStream(fileToDownload);
        response.setContentType("application/force-download");
        response.setHeader("Content-Disposition", "attachment; filename="+fileName+".txt"); 
        IOUtils.copy(inputStream, response.getOutputStream());
        response.flushBuffer();
        inputStream.close();
    } catch (Exception e){
        LOGGER.debug("Request could not be completed at this moment. Please try again.");
        e.printStackTrace();
    }

}
Sunil
źródło
14

Poniższy kod działał dla mnie w celu wygenerowania i pobrania pliku tekstowego.

@RequestMapping(value = "/download", method = RequestMethod.GET)
public ResponseEntity<byte[]> getDownloadData() throws Exception {

    String regData = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.";
    byte[] output = regData.getBytes();

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.set("charset", "utf-8");
    responseHeaders.setContentType(MediaType.valueOf("text/html"));
    responseHeaders.setContentLength(output.length);
    responseHeaders.set("Content-disposition", "attachment; filename=filename.txt");

    return new ResponseEntity<byte[]>(output, responseHeaders, HttpStatus.OK);
}
Siva Kumar
źródło
5

To, co mogę szybko wymyślić, to wygenerować pdf i zapisać go w aplikacji internetowej / download / <RANDOM-FILENAME> .pdf z kodu i wysłać dalej do tego pliku za pomocą HttpServletRequest

request.getRequestDispatcher("/downloads/<RANDOM-FILENAME>.pdf").forward(request, response);

lub jeśli możesz skonfigurować swój przelicznik widoków w taki sposób,

  <bean id="pdfViewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="viewClass"
              value="org.springframework.web.servlet.view.JstlView" />
    <property name="order" value=”2″/>
    <property name="prefix" value="/downloads/" />
    <property name="suffix" value=".pdf" />
  </bean>

to po prostu wróć

return "RANDOM-FILENAME";
kalyan
źródło
1
Jeśli potrzebuję dwóch widoków, jak mogę również zwrócić nazwę resolvera lub wybrać go w kontrolerze?
azerafati,
3

Poniższe rozwiązanie działa dla mnie

    @RequestMapping(value="/download")
    public void getLogFile(HttpSession session,HttpServletResponse response) throws Exception {
        try {

            String fileName="archivo demo.pdf";
            String filePathToBeServed = "C:\\software\\Tomcat 7.0\\tmpFiles\\";
            File fileToDownload = new File(filePathToBeServed+fileName);

            InputStream inputStream = new FileInputStream(fileToDownload);
            response.setContentType("application/force-download");
            response.setHeader("Content-Disposition", "attachment; filename="+fileName); 
            IOUtils.copy(inputStream, response.getOutputStream());
            response.flushBuffer();
            inputStream.close();
        } catch (Exception exception){
            System.out.println(exception.getMessage());
        }

    }
Jorge Santos Neill
źródło
2

coś jak poniżej

@RequestMapping(value = "/download", method = RequestMethod.GET)
public void getFile(HttpServletResponse response) {
    try {
        DefaultResourceLoader loader = new DefaultResourceLoader();
        InputStream is = loader.getResource("classpath:META-INF/resources/Accepted.pdf").getInputStream();
        IOUtils.copy(is, response.getOutputStream());
        response.setHeader("Content-Disposition", "attachment; filename=Accepted.pdf");
        response.flushBuffer();
    } catch (IOException ex) {
        throw new RuntimeException("IOError writing file to output stream");
    }
}

Możesz wyświetlić plik PDF lub pobrać przykłady tutaj

xxy
źródło
1

Jeśli to pomoże komukolwiek. Możesz zrobić to, co sugeruje zaakceptowana odpowiedź Infeligo, ale po prostu włóż ten dodatkowy fragment do kodu, aby wymusić pobranie.

response.setContentType("application/force-download");
Sagar Nair
źródło
0

W moim przypadku generuję jakiś plik na żądanie, więc trzeba też wygenerować adres URL.

Dla mnie działa coś takiego:

@RequestMapping(value = "/files/{filename:.+}", method = RequestMethod.GET, produces = "text/csv")
@ResponseBody
public FileSystemResource getFile(@PathVariable String filename) {
    String path = dataProvider.getFullPath(filename);
    return new FileSystemResource(new File(path));
}

Bardzo ważny jest typ MIME, producesa także, że nazwa pliku jest częścią linku, więc musisz go użyć @PathVariable.

Kod HTML wygląda następująco:

<a th:href="@{|/dbreport/files/${file_name}|}">Download</a>

Gdzie ${file_name}są generowane przez Thymeleaf w sterowniku i jest tzn result_20200225.csv, więc linkujące cały url behing jest: example.com/aplication/dbreport/files/result_20200225.csv.

Po kliknięciu w link przeglądarka pyta mnie, co zrobić z plikiem - zapisz lub otwórz.

Tomasz Dzięcielewski
źródło