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

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

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

16 ответов

Решение
@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");

Мне удалось поточить это, используя встроенную поддержку 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)); 
}

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

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

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

С 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) тогда было бы разумно НЕ делать его сначала байтовым массивом. Вместо этого лучше использовать:

  • InputStreamResource,
  • PathResource (начиная с весны 4.0) или
  • FileSystemResource,

пример с 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);
}

Если ты:

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

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

@RequestMapping(value = "/stuff/{stuffId}", method = RequestMethod.GET)
public ResponseEntity<InputStreamResource> downloadStuff(@PathVariable int stuffId)
                                                                  throws IOException {
    String fullPath = stuffService.figureOutFileNameFor(stuffId);
    File file = new File(fullPath);

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

    InputStreamResource isr = new InputStreamResource(new FileInputStream(file));
    return new ResponseEntity<InputStreamResource>(isr, respHeaders, HttpStatus.OK);
}

Также обратите внимание, что для того, чтобы не читать весь файл только для того, чтобы рассчитать его длину, лучше сохранить его ранее. Убедитесь, что вы проверите документы для InputStreamResource,

Tl;dr

  1. Возвращение ResponseEntity<Resource> из метода обработчика
  2. Уточнить Content-Type явно
  3. Задавать Content-Disposition если необходимо:
    1. имя файла
    2. тип
      1. inline для принудительного предварительного просмотра в браузере
      2. attachment заставить скачивать
@Controller
public class DownloadController {
    @GetMapping("/downloadPdf.pdf")
    // 1.
    public ResponseEntity<Resource> downloadPdf() {
        FileSystemResource resource = new FileSystemResource("/home/caco3/Downloads/JMC_Tutorial.pdf");
        // 2.
        MediaType mediaType = MediaTypeFactory
                .getMediaType(resource)
                .orElse(MediaType.APPLICATION_OCTET_STREAM);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(mediaType);
        // 3
        ContentDisposition disposition = ContentDisposition
                // 3.2
                .builder("inline")
                // 3.1
                .filename(resource.getFilename())
                .build();
        headers.setContentDisposition(disposition);
        return new ResponseEntity<>(resource, headers, HttpStatus.OK);
    }
}

Объяснение

Возвращение ResponseEntity<Resouce>

Ты можешь вернуться ResponseEntity<Resource> из метода обработчика. В этом случае ResourceHttpMessageConverter срабатывает и пишет соответствующий ответ.

Помните о возможном неправильном Content-Typeнабор заголовков (см. FileSystemResource возвращается с типом содержимого json). Вот почему этот ответ предлагает установитьContent-Type явно.

Уточнить Content-Typeявно:

Вот некоторые варианты:

  • жестко закодируйте заголовок
  • использовать MediaTypeFactory с весны.
  • или положитесь на стороннюю библиотеку, такую ​​как Apache Tika

В MediaTypeFactory позволяет открыть MediaType подходит для Resource (смотрите также /org/springframework/http/mime.types файл)

Задавать Content-Dispositionпри необходимости:

Иногда необходимо принудительно загрузить файл в браузере или открыть файл в браузере для предварительного просмотра. Для таких целей Content-Disposition заголовок может использоваться:

Первый параметр в контексте HTTP - это либо inline (значение по умолчанию, указывающее, что он может отображаться внутри веб-страницы или как веб-страница) или attachment (с указанием, что он должен быть загружен; в большинстве браузеров отображается диалоговое окно "Сохранить как", предварительно заполненное значением параметров имени файла, если оно есть).

В Spring Framework a ContentDisposition может быть использован.

Чтобы просмотреть файл в браузере:

ContentDisposition disposition = ContentDisposition.builder("inline")
        .filename(resource.getFilename())
        .build();

Чтобы принудительно загрузить:

ContentDisposition disposition = ContentDisposition.builder("attachment")
        .filename(resource.getFilename())
        .build();

Использовать InputStreamResourceвнимательно:

Поскольку InputStream можно прочитать только один раз, Spring не будет писать Content-Length заголовок, если вы вернете InputStreamResource (код из ResourceHttpMessageConverter):

@Override
protected Long getContentLength(Resource resource, @Nullable MediaType contentType) throws IOException {
    // Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
    // Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
    if (InputStreamResource.class == resource.getClass()) {
        return null;
    }
    long contentLength = resource.contentLength();
    return (contentLength < 0 ? null : contentLength);
}

В остальных случаях работает нормально:

~ $ curl -I localhost:8080/downloadPdf.pdf  | grep "Content-Length"
Content-Length: 7554270

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

}

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

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

Я могу быстро придумать, сгенерировать pdf и сохранить его в файле webapp/downloads/.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";

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

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

    }

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

@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 или скачать его примеры здесь

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

response.setContentType("application/force-download");

Мне пришлось добавить это, чтобы загрузить любой файл

          response.setContentType("application/octet-stream");
    response.setHeader("Content-Disposition",
            "attachment;filename="+"file.txt");

весь код:

      @Controller
public class FileController {

@RequestMapping(value = "/file", method =RequestMethod.GET)
@ResponseBody
public FileSystemResource getFile(HttpServletResponse response) {

    final File file = new File("file.txt");
    response.setContentType("application/octet-stream");
    response.setHeader("Content-Disposition",
            "attachment;filename="+"file.txt");
    return new FileSystemResource(file);
 }
}

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

Очень важен пантомима в 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.

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

Очень простой способ сделать это с помощью SpringBoot:

      import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;    

@GetMapping("/file/{fileName}")
public ResponseEntity<byte[]> getFile(@PathVariable String fileName) {
        // Create Headers for "forcing download"
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);
        // Headers for giving a custom name to the file and also the file extension, in this example .zip
        httpHeaders.set(HttpHeaders.CONTENT_DISPOSITION,
            ContentDisposition.attachment().filename(String.format("%s.%s", fileName, "zip")).build().toString());
        // Get the bytes from your service (for example an aws bucket)
        return ResponseEntity.ok().headers(httpHeaders).body(service.getFile(fileName));
    }

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

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

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

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

Другие вопросы по тегам