Herunterladen einer Datei von Federreglern

387

Ich habe eine Anforderung, bei der ich ein PDF von der Website herunterladen muss. Das PDF muss innerhalb des Codes generiert werden, was meiner Meinung nach eine Kombination aus Freemarker und einem PDF-Generierungsframework wie iText wäre. Gibt es einen besseren Weg?

Mein Hauptproblem ist jedoch, wie ich dem Benutzer erlaube, eine Datei über einen Spring Controller herunterzuladen.

MilindaD
quelle
2
Es ist erwähnenswert, dass sich Spring Framework seit 2011 stark verändert hat, so dass Sie es auch auf reaktive Weise tun können - hier ein Beispiel
Krzysztof Skrzynecki

Antworten:

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

}

Im Allgemeinen response.getOutputStream()können Sie dort alles schreiben , wenn Sie haben . Sie können diesen Ausgabestream als Speicherort für generierte PDF-Dateien an Ihren Generator übergeben. Wenn Sie wissen, welchen Dateityp Sie senden, können Sie auch festlegen

response.setContentType("application/pdf");
Infeligo
quelle
4
Dies ist ziemlich genau das, was ich sagen wollte, aber Sie sollten wahrscheinlich auch den Header des Antworttyps auf einen für die Datei geeigneten Wert setzen.
GaryF
2
Ja, habe gerade den Beitrag bearbeitet. Ich hatte verschiedene Dateitypen generiert und überließ es dem Browser, den Inhaltstyp der Datei anhand ihrer Erweiterung zu bestimmen.
Infeligo
Ich habe den FlushBuffer vergessen, dank deinem Beitrag habe ich gesehen, warum meiner nicht funktioniert hat :-)
Jan Vladimir Mostert
35
Gibt es einen besonderen Grund, Apache IOUtilsanstelle von Spring zu verwenden FileCopyUtils?
Powerlord
3
Hier ist eine bessere Lösung: stackoverflow.com/questions/16652760/…
Dmytro Plekhotkin
290

Ich konnte dies mithilfe der im Frühjahr integrierten Unterstützung mit dem ResourceHttpMessageConverter optimieren. Dadurch werden die Inhaltslänge und der Inhaltstyp festgelegt, wenn der MIME-Typ bestimmt werden kann

@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
quelle
10
Das funktioniert. Die Datei (CSV-Datei) wird jedoch im Browser angezeigt und nicht heruntergeladen. Wie kann ich den Browser zum Herunterladen zwingen?
Chzbrgla
41
Sie können produkte = MediaType.APPLICATION_OCTET_STREAM_VALUE zum @RequestMapping hinzufügen, um den Download zu erzwingen
David Kago
8
Außerdem sollten Sie <bean class = "org.springframework.http.converter.ResourceHttpMessageConverter" /> zur Liste der messageConverters hinzufügen (<mvc: annotationsgesteuert> <mvc: message-converter>)
Sllouyssgort
4
Gibt es eine Möglichkeit, den Content-DispositionHeader auf diese Weise festzulegen?
Ralph
8
Ich hatte keine Notwendigkeit dafür, aber ich denke, Sie könnten HttpResponse als Parameter zur Methode hinzufügen und dann "response.setHeader (" Content-Disposition "," Anhang; Dateiname = somefile.pdf ");"
Scott Carlson
82

Sie sollten in der Lage sein, die Datei direkt in die Antwort zu schreiben. Etwas wie

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

und schreiben Sie die Datei dann als Binär-Stream auf response.getOutputStream(). Denken Sie daran, response.flush()am Ende zu tun, und das sollte es tun.

Hummer1234
quelle
8
Ist es nicht die 'Spring'-Methode, den Inhaltstyp so festzulegen? @RequestMapping(value = "/foo/bar", produces = "application/pdf")
Schwarz
4
@Francis was ist, wenn Ihre Anwendung verschiedene Dateitypen herunterlädt? Mit der Antwort von Lobster1234 können Sie die Inhaltsdisposition dynamisch festlegen.
Rose
2
Das stimmt @Rose, aber ich glaube, es wäre besser, verschiedene Endpunkte pro Format zu definieren
Black
3
Ich denke nicht, weil es nicht skalierbar ist. Wir unterstützen derzeit ein Dutzend Arten von Ressourcen. In diesem Fall unterstützen wir möglicherweise mehr Dateitypen, je nachdem, was Benutzer hochladen möchten. Möglicherweise haben wir so viele Endpunkte, die im Wesentlichen dasselbe tun. IMHO muss es nur einen Download-Endpunkt geben und es werden eine Vielzahl von Dateitypen verarbeitet. @ Francis
Rose
3
Es ist absolut "skalierbar", aber wir können uns darauf einigen, nicht zuzustimmen, ob es die beste Vorgehensweise ist
Black
74

Mit Spring 3.0 können Sie das HttpEntityRückgabeobjekt verwenden. Wenn Sie dies verwenden, benötigt Ihr Controller kein HttpServletResponseObjekt und ist daher einfacher zu testen. Abgesehen davon ist diese Antwort relativ gleich der von Infeligo .

Wenn der Rückgabewert Ihres PDF-Frameworks ein Byte-Array ist (lesen Sie den zweiten Teil meiner Antwort für andere Rückgabewerte) :

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

Wenn der Rückgabetyp Ihres PDF Framework ( documentBbody) noch kein Byte-Array ist (und auch kein Nein ByteArrayInputStream), ist es ratsam , es NICHT zuerst zu einem Byte-Array zu machen. Stattdessen ist es besser zu verwenden:

Beispiel mit 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
quelle
11
-1 Dadurch wird unnötigerweise die gesamte Datei in den Speicher geladen. Dies kann leicht zu OutOfMemoryErrors führen.
Faisal Feroz
1
@FaisalFeroz: Ja, das ist richtig, aber das Dateidokument wird trotzdem im Speicher erstellt (siehe Frage: "PDF muss im Code generiert werden"). Wie auch immer - was ist Ihre Lösung, um dieses Problem zu lösen?
Ralph
1
Sie können auch ResponseEntity verwenden, ein Super von HttpEntity, mit dem Sie den Antwort-http-Statuscode angeben können. Beispiel:return new ResponseEntity<byte[]>(documentBody, headers, HttpStatus.CREATED)
Amr Mostafa
@Amr Mostafa: ResponseEntityist eine Unterklasse von HttpEntity(aber ich verstehe) andererseits 201 CREATED ist nicht das, was ich verwenden würde, wenn ich nur eine Ansicht zu den Daten zurückgeben würde. (siehe w3.org/Protocols/rfc2616/rfc2616-sec10.html für 201 CREATED)
Ralph
1
Gibt es einen Grund, warum Sie Leerzeichen durch einen Unterstrich im Dateinamen ersetzen? Sie können es in Anführungszeichen setzen, um den tatsächlichen Namen zu senden.
Alexandru Severin
63

Wenn du:

  • Sie möchten nicht die gesamte Datei in eine laden, byte[]bevor Sie sie an die Antwort senden.
  • Möchten / müssen Sie es senden / herunterladen über InputStream;
  • Möchten Sie die volle Kontrolle über den Mime-Typ und den gesendeten Dateinamen haben?
  • Haben Sie andere @ControllerAdviceAusnahmen für Sie (oder nicht).

Der folgende Code ist genau das, was Sie brauchen:

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

Über den Teil mit der Dateilänge : File#length()sollte im allgemeinen Fall gut genug sein, aber ich dachte, ich würde diese Beobachtung machen, weil es langsam sein kann. In diesem Fall sollten Sie es vorher gespeichert haben (z. B. in der Datenbank). Zu den Fällen, in denen es langsam sein kann, gehören: Wenn die Datei groß ist, insbesondere wenn sich die Datei in einem Remote-System befindet oder etwas Ähnlicheres - vielleicht eine Datenbank.



InputStreamResource

Wenn Ihre Ressource keine Datei ist, z. B. wenn Sie die Daten aus der Datenbank abrufen, sollten Sie sie verwenden InputStreamResource. Beispiel:

    InputStreamResource isr = new InputStreamResource(new FileInputStream(file));
    return new ResponseEntity<InputStreamResource>(isr, respHeaders, HttpStatus.OK);
acdcjunior
quelle
Sie raten nicht zur Verwendung der FileSystemResource-Klasse?
Stephane
Eigentlich glaube ich, dass es in Ordnung ist, das FileSystemResourcedort zu benutzen . Es ist sogar ratsam, wenn Ihre Ressource eine Datei ist . In diesem Beispiel FileSystemResourcekann verwendet werden, wo InputStreamResourceist.
Acdcjunior
Über den Teil zur Berechnung der Dateilänge: Wenn Sie sich Sorgen machen, seien Sie nicht. File#length()sollte im allgemeinen Fall gut genug sein. Ich habe es gerade erwähnt, weil es langsam sein kann , besonders wenn sich die Datei in einem Remote-System befindet oder so etwas Ausgefeilteres - vielleicht eine Datenbank?. Aber machen Sie sich nur Sorgen, wenn es zu einem Problem wird (oder wenn Sie harte Beweise dafür haben, dass es eines wird), nicht vorher. Der Hauptpunkt ist: Sie bemühen sich, die Datei zu streamen. Wenn Sie sie vorher vorab laden müssen, macht das Streaming keinen Unterschied, oder?
acdcjunior
Warum funktioniert der obige Code bei mir nicht? Es lädt eine 0-Byte-Datei herunter. Ich habe überprüft und sichergestellt, dass ByteArray- und ResourceMessage-Konverter vorhanden sind. Vermisse ich etwas
Coding_idiot
Warum sorgen Sie sich um ByteArray- und ResourceMessage-Konverter?
Acdcjunior
20

Dieser Code funktioniert einwandfrei, um eine Datei automatisch von Spring Controller herunterzuladen, wenn Sie auf einen Link auf jsp klicken.

@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
quelle
14

Der folgende Code hat für mich funktioniert, um eine Textdatei zu generieren und herunterzuladen.

@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
quelle
5

Was mir schnell einfällt, ist, das PDF zu generieren und es in webapp / downloads / <RANDOM-FILENAME> .pdf aus dem Code zu speichern und mit HttpServletRequest eine Weiterleitung an diese Datei zu senden

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

oder wenn Sie Ihren View Resolver so konfigurieren können,

  <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>

dann kehre einfach zurück

return "RANDOM-FILENAME";
kalyan
quelle
1
Wenn ich zwei Ansichtsauflöser benötige, wie kann ich dann auch den Namen des Auflösers zurückgeben oder ihn im Controller auswählen?
Azerafati
3

Die folgende Lösung funktioniert für mich

    @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
quelle
2

so etwas wie unten

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

Sie können PDF - Anzeige oder es Beispiele Download hier

xxy
quelle
1

Wenn es jemandem hilft. Sie können das tun, was die akzeptierte Antwort von Infeligo vorgeschlagen hat, aber dieses zusätzliche Bit einfach in den Code für einen erzwungenen Download einfügen.

response.setContentType("application/force-download");
Sagar Nair
quelle
0

In meinem Fall generiere ich eine Datei bei Bedarf, daher muss auch eine URL generiert werden.

Bei mir funktioniert so etwas:

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

Sehr wichtig ist die Eingabe von MIME producesund auch, dass dieser Name der Datei Teil des Links ist, den Sie verwenden müssen @PathVariable.

HTML-Code sieht so aus:

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

Wo ${file_name}wird von Thymeleaf im Controller generiert und ist dh: result_20200225.csv, so dass der gesamte URL-Behing-Link lautet : example.com/aplication/dbreport/files/result_20200225.csv.

Nach dem Klicken auf den Link fragt mich der Browser, was ich mit der Datei machen soll - speichern oder öffnen.

Tomasz Dzięcielewski
quelle