Скачивание файла с пружинных контроллеров

387

У меня есть требование, где мне нужно скачать PDF с веб-сайта. PDF должен быть сгенерирован в коде, который, как я думал, будет комбинацией freemarker и фреймворка для создания PDF, такого как iText. Есть ли лучший способ?

Тем не менее, моя основная проблема заключается в том, как разрешить пользователю загружать файл через Spring Controller?

MilindaD
источник
2
Стоит отметить, что Spring Framework сильно изменился с 2011 года, так что вы можете сделать это и в реактивном ключе - вот пример
Krzysztof Skrzynecki

Ответы:

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

}

Вообще говоря, когда у вас есть response.getOutputStream(), вы можете написать что-нибудь там. Вы можете передать этот выходной поток как место для помещения сгенерированного PDF в ваш генератор. Кроме того, если вы знаете, какой тип файла вы отправляете, вы можете установить

response.setContentType("application/pdf");
Infeligo
источник
4
Это почти то, что я собирался сказать, но вы, вероятно, также должны установить заголовок типа ответа на что-то подходящее для файла.
GaryF
2
Да, только что отредактировал пост. У меня были сгенерированы различные типы файлов, поэтому я оставил это браузеру, чтобы определить тип содержимого файла по его расширению.
Infeligo
Забыл flushBuffer, благодаря вашему посту я понял, почему мой пост не работал :-)
Ян Владимир Мостерт
35
Есть какая-то конкретная причина использовать Apache IOUtilsвместо Spring FileCopyUtils?
Powerlord
3
Вот лучшее решение: stackoverflow.com/questions/16652760/…
Дмитрий Плехоткин
290

Мне удалось поточить это, используя встроенную поддержку Spring с ResourceHttpMessageConverter. Это установит content-length и content-type, если это может определить mime-тип

@RequestMapping(value = "/files/{file_name}", method = RequestMethod.GET)
@ResponseBody
public FileSystemResource getFile(@PathVariable("file_name") String fileName) {
    return new FileSystemResource(myService.getFileFor(fileName)); 
}
Скотт Карлсон
источник
10
Это работает. Но файл (CSV-файл) отображается в браузере и не загружается - как я могу заставить браузер загрузить?
ЧЗБРГЛА
41
Вы можете добавить продукции = MediaType.APPLICATION_OCTET_STREAM_VALUE в @RequestMapping для принудительной загрузки
Дэвид Каго
8
Также вы должны добавить <bean class = "org.springframework.http.converter.ResourceHttpMessageConverter" /> в список messageConverters (<mvc: annotation-driven> <mvc: message-
convertters
4
Есть ли способ установить Content-Dispositionзаголовок таким образом?
Ральф
8
У меня не было необходимости в этом, но я думаю, что вы можете добавить HttpResponse в качестве параметра к методу, а затем "response.setHeader (" Content-Disposition "," attachment; filename = somefile.pdf ");"
Скотт Карлсон
82

Вы должны быть в состоянии написать файл в ответе напрямую. Что-то вроде

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

а затем записать файл в виде двоичного потока response.getOutputStream(). Не забудьте сделать response.flush()в конце, и это должно сделать это.

lobster1234
источник
8
разве это не «весенний» способ установить тип контента следующим образом? @RequestMapping(value = "/foo/bar", produces = "application/pdf")
Черное
4
@Francis, что если ваше приложение загружает файлы разных типов? Ответ Lobster1234 позволяет вам динамически устанавливать расположение контента.
Роза
2
это правда @Rose, но я считаю, что было бы лучше определить разные конечные точки для формата
Black
3
Я думаю, нет, потому что это не масштабируется. В настоящее время мы поддерживаем десятки видов ресурсов. Мы могли бы поддерживать больше типов файлов в зависимости от того, что пользователи хотят загрузить, в этом случае мы можем получить столько конечных точек, которые, по сути, делают одно и то же. ИМХО, должна быть только одна конечная точка загрузки, и она обрабатывает множество типов файлов. @Francis
Роза
3
это абсолютно «масштабируемо», но мы можем согласиться не соглашаться, является ли это наилучшей практикой
Black
74

В Spring 3.0 вы можете использовать HttpEntityвозвращаемый объект. Если вы используете это, то ваш контроллер не нуждается в HttpServletResponseобъекте, и, следовательно, его проще тестировать. Кроме этого, этот ответ является относительным равен ответ Infeligo .

Если возвращаемое значение вашей pdf-фреймворки является байтовым массивом (прочитайте вторую часть моего ответа для других возвращаемых значений) :

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

Если тип возвращаемого значения вашей PDF Framework ( documentBbody) уже не является байтовым массивом (а также нет ByteArrayInputStream), то было бы разумно НЕ делать его сначала байтовым массивом. Вместо этого лучше использовать:

пример с 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);
}
Ральф
источник
11
-1 это необязательно загрузит весь файл в память, может легко вызвать ошибки OutOfMemoryErrors.
Фейсал Фероз
1
@FaisalFeroz: да, это правильно, но файл документа в любом случае создается в памяти (см. Вопрос: «PDF должен быть создан в коде»). В любом случае - как вы решите эту проблему?
Ральф
1
Вы также можете использовать ResponseEntity, который является суперпользователем HttpEntity, который позволяет вам указывать код состояния http ответа. Пример:return new ResponseEntity<byte[]>(documentBody, headers, HttpStatus.CREATED)
Амр Мостафа
@Amr Mostafa: с другой стороны, ResponseEntityэто подкласс HttpEntity(но я понимаю) 201 CREATED - это не то, что я использовал бы, когда возвращал просто представление данных. (см. w3.org/Protocols/rfc2616/rfc2616-sec10.html для 201 CREATED)
Ральф
1
Есть ли причина, по которой вы заменяете пробелы символом подчеркивания в имени файла? Вы можете заключить его в кавычки, чтобы отправить настоящее имя.
Александру Северин
63

Если ты:

  • Не хотите загружать весь файл в byte[]перед отправкой ответа;
  • Хотите / нужно отправить / скачать через InputStream;
  • Хотите иметь полный контроль над Mime Type и именем отправляемого файла;
  • Есть другие @ControllerAdviceисключения для вас (или нет).

Код ниже - то, что вам нужно:

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

О части длины файла :File#length() должно быть достаточно хорошо в общем случае, но я подумал, что сделаю это наблюдение, потому что оно может быть медленным , и в этом случае вы должны сохранить его ранее (например, в БД). Случаи, которые могут быть медленными, включают: если файл большой, особенно если файл находится в удаленной системе или что-то более сложное, например, - база данных, может быть.



InputStreamResource

Если ваш ресурс не является файлом, например, вы берете данные из БД, вы должны использовать InputStreamResource . Пример:

    InputStreamResource isr = new InputStreamResource(new FileInputStream(file));
    return new ResponseEntity<InputStreamResource>(isr, respHeaders, HttpStatus.OK);
acdcjunior
источник
Вы не советуете использовать класс FileSystemResource?
Стефан
На самом деле, я верю, что можно использовать FileSystemResourceтам. Это даже рекомендуется, если ваш ресурс представляет собой файл . В этом примере FileSystemResourceможет быть использовано где InputStreamResourceесть.
acdcjunior
Об части расчета длины файла: Если вы беспокоитесь, не беспокойтесь. File#length()должно быть достаточно хорошо в общем случае. Я только что упомянул об этом, потому что он может быть медленным , особенно если файл находится в удаленной системе или что-то более сложное, например, база данных, может быть? Но только беспокойтесь, если это станет проблемой (или если у вас есть веские доказательства, она станет такой), не раньше. Суть в том, что вы прилагаете усилия для потоковой передачи файла, если вам нужно предварительно загрузить все это раньше, тогда потоковая передача завершается без разницы, а?
acdcjunior
почему приведенный выше код не работает для меня? Он загружает файл 0 байт. Я проверил и убедился, что конвертеры ByteArray и ResourceMessage есть. Я что-то пропустил ?
coding_idiot
Почему вы беспокоитесь о преобразователях ByteArray и ResourceMessage?
acdcjunior
20

Этот код прекрасно работает для автоматической загрузки файла с контроллера Spring при нажатии на ссылку на 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();
    }

}
Сунил
источник
14

Ниже код работал для меня, чтобы сгенерировать и загрузить текстовый файл.

@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);
}
Шива Кумар
источник
5

Я могу быстро придумать, сгенерировать pdf и сохранить его в файле webapp / downloads / <RANDOM-FILENAME> .pdf из кода и отправить пересылку в этот файл с помощью HttpServletRequest.

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

или если вы можете настроить свой видоискатель что-то вроде,

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

тогда просто вернись

return "RANDOM-FILENAME";
Калян
источник
1
Если мне нужны два преобразователя представления, как я могу также вернуть имя преобразователя или выбрать его в контроллере?
Азерафати
3

Следующее решение работает для меня

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

    }
Хорхе Сантос Нил
источник
2

что-то вроде ниже

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

Вы можете отобразить PDF или скачать его примеры здесь

XXY
источник
1

Если это кому-нибудь поможет. Вы можете сделать то, что предложил принятый Infeligo ответ, но просто добавить этот дополнительный бит в код для принудительной загрузки.

response.setContentType("application/force-download");
Сагар Наир
источник
0

Это может быть полезным ответом.

Можно ли экспортировать данные в формате PDF в веб-интерфейс?

В дополнение к этому добавление размещения содержимого в качестве вложения (по умолчанию) приведет к загрузке файла. Если вы хотите просмотреть его, вам нужно установить его в строку.

Следующая большая вещь
источник
0

В моем случае я создаю некоторый файл по требованию, поэтому также должен быть создан URL.

Для меня работает нечто подобное:

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

Очень важен тип mime, producesа также то, что это имя файла является частью ссылки, поэтому вы должны использовать его @PathVariable.

HTML-код выглядит так:

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

Где ${file_name}генерируется Thymeleaf в контроллере и есть: result_20200225.csv, так что весь URL опережать ссылка: example.com/aplication/dbreport/files/result_20200225.csv.

После нажатия на ссылку браузер спросит меня, что делать с файлом - сохранить или открыть.

Томаш Дзенчелевски
источник